Merge branch 'dev' into mofox-bus

This commit is contained in:
拾风
2025-11-26 22:52:06 +08:00
committed by GitHub
7 changed files with 334 additions and 145 deletions

View File

@@ -19,6 +19,9 @@ install(extra_lines=3)
logger = get_logger("chat_stream")
# 用于存储后台任务的集合,防止被垃圾回收
_background_tasks: set[asyncio.Task] = set()
class ChatStream:
"""聊天流对象,存储一个完整的聊天上下文"""
@@ -390,6 +393,40 @@ class ChatManager:
key = "_".join(components)
return hashlib.sha256(key.encode()).hexdigest()
async def _process_message(self, message: DatabaseMessages):
"""
[新] 在消息处理流程中加入用户信息同步。
"""
# 1. 从消息中提取用户信息
user_info = getattr(message, "user_info", None)
if not user_info:
return
platform = getattr(user_info, "platform", None)
user_id = getattr(user_info, "user_id", None)
nickname = getattr(user_info, "user_nickname", None)
cardname = getattr(user_info, "user_cardname", None)
if not platform or not user_id:
return
# 2. 异步执行用户信息同步
try:
from src.person_info.person_info import get_person_info_manager
person_info_manager = get_person_info_manager()
# 创建一个后台任务来执行同步,不阻塞当前流程
sync_task = asyncio.create_task(
person_info_manager.sync_user_info(platform, user_id, nickname, cardname)
)
# 将任务添加到集合中以防止被垃圾回收
# 可以在适当的地方(如程序关闭时)清理这个集合
_background_tasks.add(sync_task)
sync_task.add_done_callback(_background_tasks.discard)
except Exception as e:
logger.error(f"创建用户信息同步任务失败: {e}")
async def get_or_create_stream(
self, platform: str, user_info: DatabaseUserInfo, group_info: DatabaseGroupInfo | None = None
) -> ChatStream:
@@ -571,23 +608,23 @@ class ChatManager:
except Exception as e:
logger.debug(f"批量写入器保存聊天流失败,使用原始方法: {e}")
# 尝试使用数据库批量调度器回退方案1
try:
from src.common.database.db_batch_scheduler import batch_update, get_batch_session
async with get_batch_session():
# 使用批量更新
result = await batch_update(
model_class=ChatStreams,
conditions={"stream_id": stream_data_dict["stream_id"]},
data=ChatManager._prepare_stream_data(stream_data_dict),
)
if result and result > 0:
stream.saved = True
logger.debug(f"聊天流 {stream.stream_id} 通过批量调度器保存成功")
return
except (ImportError, Exception) as e:
logger.debug(f"批量调度器保存聊天流失败,使用原始方法: {e}")
# 尝试使用数据库批量调度器回退方案1 - [已废弃]
# try:
# from src.common.database.optimization.batch_scheduler import batch_update, get_batch_session
#
# async with get_batch_session():
# # 使用批量更新
# result = await batch_update(
# model_class=ChatStreams,
# conditions={"stream_id": stream_data_dict["stream_id"]},
# data=ChatManager._prepare_stream_data(stream_data_dict),
# )
# if result and result > 0:
# stream.saved = True
# logger.debug(f"聊天流 {stream.stream_id} 通过批量调度器保存成功")
# return
# except (ImportError, Exception) as e:
# logger.debug(f"批量调度器保存聊天流失败,使用原始方法: {e}")
# 回退到原始方法(最终方案)
async def _db_save_stream_async(s_data_dict: dict):

View File

@@ -1333,8 +1333,8 @@ class DefaultReplyer:
),
"cross_context": asyncio.create_task(
self._time_and_run_task(
Prompt.build_cross_context(chat_id, "s4u", target_user_info),
"cross_context",
# cross_context 的构建已移至 prompt.py
asyncio.sleep(0, result=""), "cross_context"
)
),
"notice_block": asyncio.create_task(
@@ -1522,6 +1522,8 @@ class DefaultReplyer:
# 使用新的统一Prompt系统 - 创建PromptParameters
prompt_parameters = PromptParameters(
platform=platform,
user_id=user_id,
chat_scene=chat_scene_prompt,
chat_id=chat_id,
is_group_chat=is_group_chat,

View File

@@ -709,9 +709,17 @@ class Prompt:
async def _build_relation_info(self) -> dict[str, Any]:
"""构建与对话目标相关的关系信息."""
try:
# 调用静态方法来执行实际的构建逻辑
relation_info = await Prompt.build_relation_info(
self.parameters.chat_id, self.parameters.reply_to
# [重构] 直接从 PromptParameters 获取稳定的用户身份信息
platform = self.parameters.platform
user_id = self.parameters.user_id
if not platform or not user_id:
logger.warning("无法从参数中获取platform或user_id跳过关系信息构建")
return {"relation_info_block": ""}
# 调用新的、基于ID的静态方法
relation_info = await Prompt.build_relation_info_by_user_id(
self.parameters.chat_id, platform, user_id
)
return {"relation_info_block": relation_info}
except Exception as e:
@@ -1063,43 +1071,29 @@ class Prompt:
return sender, target
@staticmethod
async def build_relation_info(chat_id: str, reply_to: str) -> str:
"""构建关于回复目标用户的关系信息字符串.
Args:
chat_id: 当前聊天的ID。
reply_to: 被回复的原始消息字符串。
Returns:
str: 格式化后的关系信息字符串,或在失败时返回空字符串。
async def build_relation_info_by_user_id(chat_id: str, platform: str, user_id: str) -> str:
"""
[新] 根据用户ID构建关系信息字符串。
"""
from src.person_info.relationship_fetcher import relationship_fetcher_manager
person_info_manager = get_person_info_manager()
person_id = person_info_manager.get_person_id(platform, user_id)
if not person_id:
logger.warning(f"构建关系信息时未找到用户 platform={platform}, user_id={user_id}")
return f"你似乎还不认识这位用户ID: {user_id}),这是你们的第一次互动。"
relationship_fetcher = relationship_fetcher_manager.get_fetcher(chat_id)
if not reply_to:
return ""
# 解析出回复目标的发送者
sender, text = Prompt.parse_reply_target(reply_to)
if not sender or not text:
return ""
# 根据发送者名称查找其用户ID
person_info_manager = get_person_info_manager()
person_id = await person_info_manager.get_person_id_by_person_name(sender)
if not person_id:
logger.warning(f"未找到用户 {sender} 的ID跳过信息提取")
return f"你完全不认识{sender}不理解ta的相关信息。"
# 使用关系提取器构建用户关系信息和聊天流印象
user_relation_info = await relationship_fetcher.build_relation_info(
person_id, points_num=5
)
stream_impression = await relationship_fetcher.build_chat_stream_impression(
chat_id
# 并行构建用户信息和聊天流印象
user_relation_info_task = relationship_fetcher.build_relation_info(person_id, points_num=5)
stream_impression_task = relationship_fetcher.build_chat_stream_impression(chat_id)
user_relation_info, stream_impression = await asyncio.gather(
user_relation_info_task, stream_impression_task
)
# 组合两部分信息
info_parts = []
if user_relation_info:
info_parts.append(user_relation_info)
@@ -1149,6 +1143,7 @@ class Prompt:
Returns:
str: 构建好的跨群聊上下文字符串。
"""
logger.info(f"Building cross context with target_user_info: {target_user_info}")
if not global_config.cross_context.enable:
return ""
@@ -1167,32 +1162,22 @@ class Prompt:
return ""
@staticmethod
async def parse_reply_target_id(reply_to: str) -> str:
"""从回复目标字符串中解析出原始发送者的用户ID.
Args:
reply_to: 回复目标字符串。
Returns:
str: 找到的用户ID如果找不到则返回空字符串。
"""
if not reply_to:
return ""
# 首先,解析出发送者的名称
sender, _ = Prompt.parse_reply_target(reply_to)
if not sender:
return ""
# 然后通过名称查询用户ID
person_info_manager = get_person_info_manager()
person_id = await person_info_manager.get_person_id_by_person_name(sender)
if person_id:
user_id = await person_info_manager.get_value(person_id, "user_id")
return str(user_id) if user_id else ""
return ""
# [废弃] 该函数完全依赖于不稳定的名称解析,应被移除
# @staticmethod
# async def parse_reply_target_id(reply_to: str) -> str:
# """从回复目标字符串中解析出原始发送者的用户ID."""
# if not reply_to:
# return ""
# sender, _ = Prompt.parse_reply_target(reply_to)
# if not sender:
# return ""
# person_info_manager = get_person_info_manager()
# # [脆弱点] 使用了不稳健的按名称查询
# person_id = await person_info_manager.get_person_id_by_name_robust(sender)
# if person_id:
# user_id = await person_info_manager.get_value(person_id, "user_id")
# return str(user_id) if user_id else ""
# return ""
# 工厂函数

View File

@@ -11,6 +11,8 @@ class PromptParameters:
# 基础参数
chat_id: str = ""
platform: str = ""
user_id: str = ""
is_group_chat: bool = False
sender: str = ""
target: str = ""

View File

@@ -135,6 +135,113 @@ class PersonInfoManager:
logger.error(f"根据用户名 {person_name} 获取用户ID时出错: {e}")
return ""
@staticmethod
@cached(ttl=600, key_prefix="person_info_by_user_id", use_kwargs=False)
async def get_person_info_by_user_id(platform: str, user_id: str) -> dict | None:
"""[新] 根据 platform 和 user_id 获取用户信息字典"""
if not platform or not user_id:
return None
person_id = PersonInfoManager.get_person_id(platform, user_id)
crud = CRUDBase(PersonInfo)
record = await crud.get_by(person_id=person_id)
if not record:
return None
# 将 SQLAlchemy 模型对象转换为字典
return {c.name: getattr(record, c.name) for c in record.__table__.columns}
@staticmethod
@cached(ttl=600, key_prefix="person_info_by_person_id", use_kwargs=False)
async def get_person_info_by_person_id(person_id: str) -> dict | None:
"""[新] 根据 person_id 获取用户信息字典"""
if not person_id:
return None
crud = CRUDBase(PersonInfo)
record = await crud.get_by(person_id=person_id)
if not record:
return None
# 将 SQLAlchemy 模型对象转换为字典
return {c.name: getattr(record, c.name) for c in record.__table__.columns}
@staticmethod
async def get_person_id_by_name_robust(name: str) -> str | None:
"""[新] 稳健地根据名称获取 person_id按 person_name -> nickname 顺序回退"""
if not name:
return None
crud = CRUDBase(PersonInfo)
# 1. 按 person_name 查询
records = await crud.get_multi(person_name=name, limit=1)
if records:
return records[0].person_id
# 2. 按 nickname 查询
records = await crud.get_multi(nickname=name, limit=1)
if records:
return records[0].person_id
return None
@staticmethod
@staticmethod
@cached(ttl=600, key_prefix="person_info_by_name_robust", use_kwargs=False)
async def get_person_info_by_name_robust(name: str) -> dict | None:
"""[新] 稳健地根据名称获取用户信息,按 person_name -> nickname 顺序回退"""
person_id = await PersonInfoManager.get_person_id_by_name_robust(name)
if person_id:
return await PersonInfoManager.get_person_info_by_person_id(person_id)
return None
@staticmethod
async def sync_user_info(platform: str, user_id: str, nickname: str | None, cardname: str | None) -> str:
"""
[新] 同步用户信息。查询或创建用户,并更新易变信息(如昵称)。
返回 person_id。
"""
if not platform or not user_id:
raise ValueError("platform 和 user_id 不能为空")
person_id = PersonInfoManager.get_person_id(platform, user_id)
crud = CRUDBase(PersonInfo)
record = await crud.get_by(person_id=person_id)
effective_name = cardname or nickname or "未知用户"
if record:
# 用户已存在,检查是否需要更新
updates = {}
if nickname and record.nickname != nickname:
updates["nickname"] = nickname
if updates:
await crud.update(record.id, updates)
logger.debug(f"用户 {person_id} 信息已更新: {updates}")
else:
# 用户不存在,创建新用户
logger.info(f"新用户 {platform}:{user_id},将创建记录。")
unique_person_name = await PersonInfoManager._generate_unique_person_name(effective_name)
new_person_data = {
"person_id": person_id,
"platform": platform,
"user_id": str(user_id),
"nickname": nickname,
"person_name": unique_person_name,
"name_reason": "首次遇见时自动设置",
"know_since": int(time.time()),
"last_know": int(time.time()),
}
await PersonInfoManager._safe_create_person_info(person_id, new_person_data)
return person_id
@staticmethod
@staticmethod
async def first_knowing_some_one(platform: str, user_id: str, user_nickname: str, user_cardname: str):
"""判断是否认识某人"""

View File

@@ -299,6 +299,124 @@ class ChatterPlanFilter:
)
# Prepare format parameters
# Prepare format parameters
# 根据配置动态生成回复策略和输出格式的提示词部分
if global_config.chat.enable_multiple_replies:
reply_strategy_block = """
# 目标
你的任务是根据当前对话,给出一个或多个动作,构成一次完整的响应组合。
- 主要动作:通常是 reply或respond如需回复
- 辅助动作(可选):如 emoji、poke_user 等,用于增强表达。
# 决策流程
1. 已读仅供参考,不能对已读执行任何动作。
2. 目标消息必须来自未读历史,并使用其前缀 <m...> 作为 target_message_id。
3. 兴趣度优先原则:每条未读消息后都标注了 [兴趣度: X.XXX],数值越高表示该消息越值得你关注和回复。在选择回复目标时,**应优先选择兴趣度高的消息**(通常 ≥0.5 表示较高兴趣),除非有特殊情况(如被直接@或提问)。
4. 优先级:
- 直接针对你:@你、回复你、点名提问、引用你的消息。
- **兴趣度高的消息**:兴趣度 ≥0.5 的消息应优先考虑回复。
- 与你强相关的话题或你熟悉的问题。
- 其他与上下文弱相关的内容最后考虑。
{mentioned_bonus}
5. 多目标:若多人同时需要回应,请在 actions 中并行生成多个 reply每个都指向各自的 target_message_id。
6. 处理无上下文的纯表情包: 对不含任何实质文本、且无紧密上下文互动的纯**表情包**消息(如消息内容仅为“[表情包xxxxx]”),应默认选择 `no_action`。
7. 处理失败消息: 绝不能回复任何指示媒体内容(图片、表情包等)处理失败的消息。如果消息中出现如“[表情包(描述生成失败)]”或“[图片(描述生成失败)]”等文字,必须将其视为系统错误提示,并立即选择`no_action`。
8. 正确决定回复时机: 在决定reply或respond前务必评估当前对话氛围和上下文连贯性。避免在不合适的时机如对方情绪低落、话题不相关等,对方并没有和你对话,贸然插入会很令人讨厌等)进行回复,以免打断对话流或引起误解。如判断当前不适合回复,请选择`no_action`。
9. 认清自己的身份和角色: 在规划回复时,务必确定对方是不是真的在叫自己。聊天时往往有数百甚至数千个用户,请务必认清自己的身份和角色,避免误以为对方在和自己对话而贸然插入回复,导致尴尬局面。
"""
output_format_block = """
## 输出格式(只输出 JSON不要多余文本或代码块
最终输出必须是一个包含 thinking 和 actions 字段的 JSON 对象,其中 actions 必须是一个列表。
示例(单动作):
```json
{{
"thinking": "在这里写下你的思绪流...",
"actions": [
{{
"action_type": "reply",
"reasoning": "选择该动作的详细理由",
"action_data": {{
"target_message_id": "m124",
"content": "回复内容"
}}
}}
]
}}
```
示例(多重动作,并行):
```json
{{
"thinking": "在这里写下你的思绪流...",
"actions": [
{{
"action_type": "reply",
"reasoning": "理由A - 这个消息较早且需要明确回复对象",
"action_data": {{
"target_message_id": "m124",
"content": "对A的回复",
"should_quote_reply": false
}}
}},
{{
"action_type": "reply",
"reasoning": "理由B",
"action_data": {{
"target_message_id": "m125",
"content": "对B的回复",
"should_quote_reply": false
}}
}}
]
}}
```
"""
else:
reply_strategy_block = """
# 目标
你的任务是根据当前对话,**选择一个最需要回应的目标**,并给出一个动作,构成一次完整的响应。
- 主要动作:通常是 reply如需回复
- 辅助动作(可选):如 emoji、poke_user 等,用于增强表达。
# 决策流程
1. 已读仅供参考,不能对已读执行任何动作。
2. 目标消息必须来自未读历史,并使用其前缀 <m...> 作为 target_message_id。
3. **单一目标原则**: 你必须从所有未读消息中,根据**兴趣度**和**优先级**,选择**唯一一个**最值得回应的目标。
4. 兴趣度优先原则:每条未读消息后都标注了 [兴趣度: X.XXX],数值越高表示该消息越值得你关注和回复。在选择回复目标时,**应优先选择兴趣度高的消息**(通常 ≥0.5 表示较高兴趣),除非有特殊情况(如被直接@或提问)。
5. 优先级:
- 直接针对你:@你、回复你、点名提问、引用你的消息。
- **兴趣度高的消息**:兴趣度 ≥0.5 的消息应优先考虑回复。
- 与你强相关的话题或你熟悉的问题。
- 其他与上下文弱相关的内容最后考虑。
{mentioned_bonus}
6. 处理无上下文的纯表情包: 对不含任何实质文本、且无紧密上下文互动的纯**表情包**消息(如消息内容仅为“[表情包xxxxx]”),应默认选择 `no_action`。
7. 处理失败消息: 绝不能回复任何指示媒体内容(图片、表情包等)处理失败的消息。如果消息中出现如“[表情包(描述生成失败)]”或“[图片(描述生成失败)]”等文字,必须将其视为系统错误提示,并立即选择`no_action`。
8. 正确决定回复时机: 在决定reply或respond前务必评估当前对话氛围和上下文连贯性。避免在不合适的时机如对方情绪低落、话题不相关等,对方并没有和你对话,贸然插入会很令人讨厌等)进行回复,以免打断对话流或引起误解。如判断当前不适合回复,请选择`no_action`。
9. 认清自己的身份和角色: 在规划回复时,务必确定对方是不是真的在叫自己。聊天时往往有数百甚至数千个用户,请务必认清自己的身份和角色,避免误以为对方在和自己对话而贸然插入回复,导致尴尬局面。
"""
output_format_block = """
## 输出格式(只输出 JSON不要多余文本或代码块
最终输出必须是一个包含 thinking 和 actions 字段的 JSON 对象,其中 actions 必须是一个**只包含单个动作**的列表。
示例:
```json
{{
"thinking": "在这里写下你的思绪流...",
"actions": [
{{
"action_type": "reply",
"reasoning": "选择该动作的详细理由",
"action_data": {{
"target_message_id": "m124",
"content": "回复内容"
}}
}}
]
}}
```
"""
format_params = {
"schedule_block": schedule_block,
"mood_block": mood_block,
@@ -316,6 +434,8 @@ class ChatterPlanFilter:
"custom_prompt_block": custom_prompt_block,
"bot_name": bot_name,
"users_in_chat": users_in_chat_str,
"reply_strategy_block": reply_strategy_block,
"output_format_block": output_format_block,
}
prompt = planner_prompt_template.format(**format_params)
return prompt, message_id_list

View File

@@ -41,26 +41,7 @@ def init_prompts():
{moderation_prompt}
# 目标
你的任务是根据当前对话,给出一个或多个动作,构成一次完整的响应组合。
- 主要动作:通常是 reply或respond如需回复
- 辅助动作(可选):如 emoji、poke_user 等,用于增强表达。
# 决策流程
1. 已读仅供参考,不能对已读执行任何动作。
2. 目标消息必须来自未读历史,并使用其前缀 <m...> 作为 target_message_id。
3. 兴趣度优先原则:每条未读消息后都标注了 [兴趣度: X.XXX],数值越高表示该消息越值得你关注和回复。在选择回复目标时,**应优先选择兴趣度高的消息**(通常 ≥0.5 表示较高兴趣),除非有特殊情况(如被直接@或提问)。
4. 优先级:
- 直接针对你:@你、回复你、点名提问、引用你的消息。
- **兴趣度高的消息**:兴趣度 ≥0.5 的消息应优先考虑回复。
- 与你强相关的话题或你熟悉的问题。
- 其他与上下文弱相关的内容最后考虑。
{mentioned_bonus}
5. 多目标:若多人同时需要回应,请在 actions 中并行生成多个 reply每个都指向各自的 target_message_id。
6. 处理无上下文的纯表情包: 对不含任何实质文本、且无紧密上下文互动的纯**表情包**消息(如消息内容仅为“[表情包xxxxx]”),应默认选择 `no_action`。
7. 处理失败消息: 绝不能回复任何指示媒体内容(图片、表情包等)处理失败的消息。如果消息中出现如“[表情包(描述生成失败)]”或“[图片(描述生成失败)]”等文字,必须将其视为系统错误提示,并立即选择`no_action`。
8. 正确决定回复时机: 在决定reply或respond前务必评估当前对话氛围和上下文连贯性。避免在不合适的时机如对方情绪低落、话题不相关等,对方并没有和你对话,贸然插入会很令人讨厌等)进行回复,以免打断对话流或引起误解。如判断当前不适合回复,请选择`no_action`。
9. 认清自己的身份和角色: 在规划回复时,务必确定对方是不是真的在叫自己。聊天时往往有数百甚至数千个用户,请务必认清自己的身份和角色,避免误以为对方在和自己对话而贸然插入回复,导致尴尬局面。
{reply_strategy_block}
# 思绪流规范thinking
- 真实、自然、非结论化,像给自己看的随笔。
@@ -71,52 +52,7 @@ def init_prompts():
## 可用动作列表
{action_options_text}
## 输出格式(只输出 JSON不要多余文本或代码块
最终输出必须是一个包含 thinking 和 actions 字段的 JSON 对象,其中 actions 必须是一个列表。
示例(单动作):
```json
{{
"thinking": "在这里写下你的思绪流...",
"actions": [
{{
"action_type": "reply",
"reasoning": "选择该动作的详细理由",
"action_data": {{
"target_message_id": "m124",
"content": "回复内容"
}}
}}
]
}}
```
示例(多重动作,并行):
```json
{{
"thinking": "在这里写下你的思绪流...",
"actions": [
{{
"action_type": "reply",
"reasoning": "理由A - 这个消息较早且需要明确回复对象",
"action_data": {{
"target_message_id": "m124",
"content": "对A的回复",
"should_quote_reply": false
}}
}},
{{
"action_type": "reply",
"reasoning": "理由B",
"action_data": {{
"target_message_id": "m125",
"content": "对B的回复",
"should_quote_reply": false
}}
}}
]
}}
```
{output_format_block}
# 强制规则
- 每个动作块必须包含 action_type、reasoning 和 action_data 三个字段