Merge branch 'dev' of https://github.com/MoFox-Studio/MoFox_Bot into dev
This commit is contained in:
@@ -60,7 +60,7 @@ class ChatterPlanFilter:
|
||||
prompt, used_message_id_list = await self._build_prompt(plan)
|
||||
plan.llm_prompt = prompt
|
||||
if global_config.debug.show_prompt:
|
||||
logger.info(f"规划器原始提示词:{prompt}") #叫你不要改你耳朵聋吗😡😡😡😡😡
|
||||
logger.debug(f"规划器原始提示词:{prompt}")
|
||||
|
||||
llm_content, _ = await self.planner_llm.generate_response_async(prompt=prompt)
|
||||
|
||||
|
||||
@@ -162,6 +162,16 @@ class MessageHandler:
|
||||
)
|
||||
logger.debug(f"原始消息内容: {raw_message.get('message', [])}")
|
||||
|
||||
# 检查是否包含@或video消息段
|
||||
message_segments = raw_message.get("message", [])
|
||||
if message_segments:
|
||||
for i, seg in enumerate(message_segments):
|
||||
seg_type = seg.get("type")
|
||||
if seg_type in ["at", "video"]:
|
||||
logger.info(f"检测到 {seg_type.upper()} 消息段 [{i}]: {seg}")
|
||||
elif seg_type not in ["text", "face", "image"]:
|
||||
logger.warning(f"检测到特殊消息段 [{i}]: type={seg_type}, data={seg.get('data', {})}")
|
||||
|
||||
message_type: str = raw_message.get("message_type")
|
||||
message_id: int = raw_message.get("message_id")
|
||||
# message_time: int = raw_message.get("time")
|
||||
|
||||
@@ -237,6 +237,7 @@ class SendHandler:
|
||||
target_id = str(target_id)
|
||||
if target_id == "notice":
|
||||
return payload
|
||||
logger.info(target_id if isinstance(target_id, str) else "")
|
||||
new_payload = self.build_payload(
|
||||
payload,
|
||||
await self.handle_reply_message(target_id if isinstance(target_id, str) else "", user_info),
|
||||
@@ -321,7 +322,7 @@ class SendHandler:
|
||||
# 如果没有获取到被回复者的ID,则直接返回,不进行@
|
||||
if not replied_user_id:
|
||||
logger.warning(f"无法获取消息 {id} 的发送者信息,跳过 @")
|
||||
logger.debug(f"最终返回的回复段: {reply_seg}")
|
||||
logger.info(f"最终返回的回复段: {reply_seg}")
|
||||
return reply_seg
|
||||
|
||||
# 根据概率决定是否艾特用户
|
||||
@@ -339,7 +340,7 @@ class SendHandler:
|
||||
logger.info(f"最终返回的回复段: {reply_seg}")
|
||||
return reply_seg
|
||||
|
||||
logger.debug(f"最终返回的回复段: {reply_seg}")
|
||||
logger.info(f"最终返回的回复段: {reply_seg}")
|
||||
return reply_seg
|
||||
|
||||
def handle_text_message(self, message: str) -> dict:
|
||||
|
||||
@@ -21,6 +21,8 @@ from src.plugin_system.apis import (
|
||||
send_api,
|
||||
)
|
||||
|
||||
from .prompts import DECISION_PROMPT, PLAN_PROMPT
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@@ -80,7 +82,51 @@ class ProactiveThinkerExecutor:
|
||||
)
|
||||
logger.info(f"决策结果为:回复。话题: {topic}")
|
||||
|
||||
plan_prompt = self._build_plan_prompt(context, start_mode, topic, reason)
|
||||
# 根据聊天类型构建特定上下文
|
||||
if context["chat_type"] == "private":
|
||||
user_info = context["user_info"]
|
||||
relationship = context["relationship"]
|
||||
target_user_or_group = f"你的朋友 '{user_info.user_nickname}'"
|
||||
context_specific_block = f"""
|
||||
1. **你的日程**:
|
||||
{context["schedule_context"]}
|
||||
2. **你和Ta的关系**:
|
||||
- 详细印象: {relationship["impression"]}
|
||||
- 好感度: {relationship["attitude"]}/100
|
||||
3. **最近的聊天摘要**:
|
||||
{context["recent_chat_history"]}
|
||||
4. **你最近的相关动作**:
|
||||
{context["action_history_context"]}
|
||||
"""
|
||||
else: # group
|
||||
group_info = context["group_info"]
|
||||
target_user_or_group = f"群聊 '{group_info['group_name']}'"
|
||||
context_specific_block = f"""
|
||||
1. **你的日程**:
|
||||
{context["schedule_context"]}
|
||||
2. **群聊信息**:
|
||||
- 群名称: {group_info["group_name"]}
|
||||
3. **最近的聊天摘要**:
|
||||
{context["recent_chat_history"]}
|
||||
4. **你最近的相关动作**:
|
||||
{context["action_history_context"]}
|
||||
"""
|
||||
|
||||
plan_prompt = PLAN_PROMPT.format(
|
||||
bot_nickname=global_config.bot.nickname,
|
||||
persona_core=context["persona"]["core"],
|
||||
persona_side=context["persona"]["side"],
|
||||
identity=context["persona"]["identity"],
|
||||
current_time=context["current_time"],
|
||||
target_user_or_group=target_user_or_group,
|
||||
reason=reason,
|
||||
topic=topic,
|
||||
context_specific_block=context_specific_block,
|
||||
mood_state=context["mood_state"],
|
||||
)
|
||||
|
||||
if global_config.debug.show_prompt:
|
||||
logger.info(f"主动思考回复器原始提示词:{plan_prompt}")
|
||||
|
||||
is_success, response, _, _ = await llm_api.generate_with_model(
|
||||
prompt=plan_prompt, model_config=model_config.model_task_config.replyer
|
||||
@@ -222,150 +268,54 @@ class ProactiveThinkerExecutor:
|
||||
logger.warning(f"Stream {stream_id} 既没有 group_info 也没有 user_info")
|
||||
return None
|
||||
|
||||
def _build_decision_prompt(self, context: dict[str, Any], start_mode: str) -> str:
|
||||
"""
|
||||
根据收集到的上下文信息,构建用于决策的提示词。
|
||||
|
||||
Args:
|
||||
context: 包含所有上下文信息的字典。
|
||||
start_mode: 启动模式 ('cold_start' 或 'wake_up')。
|
||||
|
||||
Returns:
|
||||
构建完成的决策提示词字符串。
|
||||
"""
|
||||
chat_type = context["chat_type"]
|
||||
persona = context["persona"]
|
||||
|
||||
# 构建通用头部
|
||||
prompt = f"""
|
||||
# 角色
|
||||
你的名字是{global_config.bot.nickname},你的人设如下:
|
||||
- 核心人设: {persona["core"]}
|
||||
- 侧面人设: {persona["side"]}
|
||||
- 身份: {persona["identity"]}
|
||||
|
||||
你的当前情绪状态是: {context["mood_state"]}
|
||||
|
||||
# 你最近的相关决策历史 (供参考)
|
||||
{context["action_history_context"]}
|
||||
"""
|
||||
# 根据聊天类型构建任务和情境
|
||||
if chat_type == "private":
|
||||
user_info = context["user_info"]
|
||||
relationship = context["relationship"]
|
||||
prompt += f"""
|
||||
# 任务
|
||||
现在是 {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. **和Ta在别处的讨论摘要**:
|
||||
{context["cross_context_block"]}
|
||||
5. **最近的聊天摘要**:
|
||||
{context["recent_chat_history"]}
|
||||
"""
|
||||
elif chat_type == "group":
|
||||
group_info = context["group_info"]
|
||||
prompt += f"""
|
||||
# 任务
|
||||
现在是 {context["current_time"]},你需要根据当前的情境,决定是否要主动向群聊 '{group_info["group_name"]}' 发起对话。
|
||||
|
||||
# 情境分析
|
||||
1. **启动模式**: {start_mode} ({"首次加入/很久未发言" if start_mode == "cold_start" else "日常唤醒"})
|
||||
2. **你的日程**:
|
||||
{context["schedule_context"]}
|
||||
3. **群聊信息**:
|
||||
- 群名称: {group_info["group_name"]}
|
||||
4. **最近的聊天摘要**:
|
||||
{context["recent_chat_history"]}
|
||||
"""
|
||||
# 构建通用尾部
|
||||
prompt += """
|
||||
# 决策目标
|
||||
你的最终目标是根据你的角色和当前情境,做出一个最符合人类社交直觉的决策,以求:
|
||||
- **(私聊)深化关系**: 通过展现你的关心、记忆和个性来拉近与对方的距离。
|
||||
- **(群聊)活跃气氛**: 提出能引起大家兴趣的话题,促进群聊的互动。
|
||||
- **提供价值**: 你的出现应该是有意义的,无论是情感上的温暖,还是信息上的帮助。
|
||||
- **保持自然**: 避免任何看起来像机器人或骚扰的行为。
|
||||
|
||||
# 决策指令
|
||||
请综合以上所有信息,以稳定、真实、拟人的方式做出决策。你的决策需要以JSON格式输出,包含以下字段:
|
||||
- `should_reply`: bool, 是否应该发起对话。
|
||||
- `topic`: str, 如果 `should_reply` 为 true,你打算聊什么话题?
|
||||
- `reason`: str, 做出此决策的简要理由,需体现你对上述目标的考量。
|
||||
|
||||
# 决策流程与核心原则
|
||||
1. **检查对话状态**:
|
||||
- **最后发言者**: 查看【最近的聊天摘要】。如果最后一条消息是你发的,且对方尚未回复,**通常应选择不回复**。这是最重要的原则,以避免打扰。
|
||||
- **例外**: 只有在等待时间足够长(例如超过数小时),或者你有非常重要且有时效性的新话题(例如,“你昨晚说的那个电影我刚看了!”)时,才考虑再次发言。
|
||||
- **无人发言**: 如果最近的聊天记录里只有你一个人在说话,**绝对不要回复**,以防刷屏。
|
||||
|
||||
2. **寻找话题切入点 (如果可以回复)**:
|
||||
- **强关联优先**: 优先从【情境分析】中寻找最自然、最相关的话题。顺序建议:`最近的聊天摘要` > `你和Ta的关系` > `你的日程`。一个好的话题往往是对最近对话的延续。
|
||||
- **展现个性**: 结合你的【人设】和【情绪】,思考你会如何看待这些情境信息,并从中找到话题。例如,如果你是一个活泼的人,看到对方日程很满,可以说:“看你今天日程满满,真是活力四射的一天呀!”
|
||||
- **备选方案**: 如果实在没有强关联的话题,可以发起一个简单的日常问候,如“在吗?”或“下午好”。
|
||||
|
||||
3. **最终决策**:
|
||||
- **权衡频率**: 查看【你最近的相关决策历史】。如果你在短时间内已经主动发起过多次对话,即使现在有话题,也应倾向于**不回复**,保持一定的社交距离。
|
||||
- **质量胜于数量**: 宁可错过一次普通的互动机会,也不要进行一次尴尬或生硬的对话。
|
||||
|
||||
|
||||
---
|
||||
示例1 (基于上下文):
|
||||
{{
|
||||
"should_reply": true,
|
||||
"topic": "关心一下Ta昨天提到的那个项目进展如何了",
|
||||
"reason": "用户昨天在聊天中提到了一个重要的项目,现在主动关心一下进展,会显得很体贴,也能自然地开启对话。"
|
||||
}}
|
||||
|
||||
示例2 (简单问候):
|
||||
{{
|
||||
"should_reply": true,
|
||||
"topic": "打个招呼,问问Ta现在在忙些什么",
|
||||
"reason": "最近没有聊天记录,日程也很常规,没有特别的切入点。一个简单的日常问候是最安全和自然的方式来重新连接。"
|
||||
}}
|
||||
|
||||
示例3 (不应回复 - 过于频繁):
|
||||
{{
|
||||
"should_reply": false,
|
||||
"topic": null,
|
||||
"reason": "虽然群里很活跃,但现在是深夜,而且最近的聊天话题我也不熟悉,没有合适的理由去打扰大家。"
|
||||
}}
|
||||
|
||||
示例4 (不应回复 - 等待回应):
|
||||
{{
|
||||
"should_reply": false,
|
||||
"topic": null,
|
||||
"reason": "我注意到上一条消息是我几分钟前主动发送的,对方可能正在忙。为了表现出耐心和体贴,我现在最好保持安静,等待对方的回应。"
|
||||
}}
|
||||
---
|
||||
|
||||
请输出你的决策:
|
||||
"""
|
||||
return prompt
|
||||
|
||||
async def _make_decision(self, context: dict[str, Any], start_mode: str) -> dict[str, Any] | None:
|
||||
"""
|
||||
调用 LLM 进行决策,判断是否应该主动发起对话,以及聊什么话题。
|
||||
|
||||
Args:
|
||||
context: 包含所有上下文信息的字典。
|
||||
start_mode: 启动模式。
|
||||
|
||||
Returns:
|
||||
一个包含决策结果的字典 (例如: {"should_reply": bool, "topic": str, "reason": str}),
|
||||
如果决策过程失败则返回 None 或包含错误信息的字典。
|
||||
"""
|
||||
if context["chat_type"] not in ["private", "group"]:
|
||||
return {"should_reply": False, "reason": "未知的聊天类型"}
|
||||
|
||||
prompt = self._build_decision_prompt(context, start_mode)
|
||||
# 根据聊天类型构建特定上下文
|
||||
if context["chat_type"] == "private":
|
||||
user_info = context["user_info"]
|
||||
relationship = context["relationship"]
|
||||
target_user_or_group = f"用户 '{user_info.user_nickname}'"
|
||||
context_specific_block = f"""
|
||||
1. **启动模式**: {start_mode} ({"初次见面/很久未见" if start_mode == "cold_start" else "日常唤醒"})
|
||||
2. **你的日程**:
|
||||
{context["schedule_context"]}
|
||||
3. **你和Ta的关系**:
|
||||
- 简短印象: {relationship["short_impression"]}
|
||||
- 详细印象: {relationship["impression"]}
|
||||
- 好感度: {relationship["attitude"]}/100
|
||||
4. **和Ta在别处的讨论摘要**:
|
||||
{context["cross_context_block"]}
|
||||
5. **最近的聊天摘要**:
|
||||
{context["recent_chat_history"]}
|
||||
"""
|
||||
else: # group
|
||||
group_info = context["group_info"]
|
||||
target_user_or_group = f"群聊 '{group_info['group_name']}'"
|
||||
context_specific_block = f"""
|
||||
1. **启动模式**: {start_mode} ({"首次加入/很久未发言" if start_mode == "cold_start" else "日常唤醒"})
|
||||
2. **你的日程**:
|
||||
{context["schedule_context"]}
|
||||
3. **群聊信息**:
|
||||
- 群名称: {group_info["group_name"]}
|
||||
4. **最近的聊天摘要**:
|
||||
{context["recent_chat_history"]}
|
||||
"""
|
||||
prompt = DECISION_PROMPT.format(
|
||||
bot_nickname=global_config.bot.nickname,
|
||||
persona_core=context["persona"]["core"],
|
||||
persona_side=context["persona"]["side"],
|
||||
identity=context["persona"]["identity"],
|
||||
mood_state=context["mood_state"],
|
||||
action_history_context=context["action_history_context"],
|
||||
current_time=context["current_time"],
|
||||
target_user_or_group=target_user_or_group,
|
||||
context_specific_block=context_specific_block,
|
||||
)
|
||||
|
||||
if global_config.debug.show_prompt:
|
||||
logger.info(f"主动思考决策器原始提示词:{prompt}")
|
||||
@@ -385,160 +335,3 @@ class ProactiveThinkerExecutor:
|
||||
except orjson.JSONDecodeError:
|
||||
logger.error(f"决策LLM返回的JSON格式无效: {response}")
|
||||
return {"should_reply": False, "reason": "决策模型返回格式错误"}
|
||||
|
||||
def _build_private_plan_prompt(self, context: dict[str, Any], start_mode: str, topic: str, reason: str) -> str:
|
||||
"""
|
||||
为私聊场景构建生成对话内容的规划提示词。
|
||||
|
||||
Args:
|
||||
context: 上下文信息字典。
|
||||
start_mode: 启动模式。
|
||||
topic: 决策模块决定的话题。
|
||||
reason: 决策模块给出的理由。
|
||||
|
||||
Returns:
|
||||
构建完成的私聊规划提示词字符串。
|
||||
"""
|
||||
user_info = context["user_info"]
|
||||
relationship = context["relationship"]
|
||||
if start_mode == "cold_start":
|
||||
return f"""
|
||||
# 任务
|
||||
你需要主动向一个新朋友 '{user_info.user_nickname}' 发起对话。这是你们的第一次交流,或者很久没聊了。
|
||||
|
||||
# 决策上下文
|
||||
- **决策理由**: {reason}
|
||||
|
||||
# 情境分析
|
||||
1. **你的日程**:
|
||||
{context["schedule_context"]}
|
||||
2. **你和Ta的关系**:
|
||||
- 简短印象: {relationship["short_impression"]}
|
||||
- 详细印象: {relationship["impression"]}
|
||||
- 好感度: {relationship["attitude"]}/100
|
||||
3. **和Ta在别处的讨论摘要**:
|
||||
{context["cross_context_block"]}
|
||||
4. **最近的聊天摘要**:
|
||||
{context["recent_chat_history"]}
|
||||
5. **你最近的相关动作**:
|
||||
{context["action_history_context"]}
|
||||
|
||||
# 对话指引
|
||||
- 你的目标是“破冰”,让对话自然地开始。
|
||||
- 你应该围绕这个话题展开: {topic}
|
||||
- 你的语气应该符合你的人设和你当前的心情({context["mood_state"]}),友好且真诚。
|
||||
"""
|
||||
else: # wake_up
|
||||
return f"""
|
||||
# 任务
|
||||
现在是 {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的印象)巧妙地融入到对话中,让你的话语听起来更真实、更有依据。
|
||||
- **符合人设**: 你的语气、用词、甚至表情符号的使用,都应该完全符合你的【角色】设定和当前【情绪】({context["mood_state"]})以及你对Ta的好感度。
|
||||
- 请结合以上所有情境信息,自然地开启对话。
|
||||
"""
|
||||
|
||||
def _build_group_plan_prompt(self, context: dict[str, Any], topic: str, reason: str) -> str:
|
||||
"""
|
||||
为群聊场景构建生成对话内容的规划提示词。
|
||||
|
||||
Args:
|
||||
context: 上下文信息字典。
|
||||
topic: 决策模块决定的话题。
|
||||
reason: 决策模块给出的理由。
|
||||
|
||||
Returns:
|
||||
构建完成的群聊规划提示词字符串。
|
||||
"""
|
||||
group_info = context["group_info"]
|
||||
return f"""
|
||||
# 任务
|
||||
现在是 {context["current_time"]},你需要主动向群聊 '{group_info["group_name"]}' 发起对话。
|
||||
|
||||
# 决策上下文
|
||||
- **决策理由**: {reason}
|
||||
|
||||
# 情境分析
|
||||
1. **你的日程**:
|
||||
你当前的心情({context["mood_state"]}
|
||||
{context["schedule_context"]}
|
||||
2. **群聊信息**:
|
||||
- 群名称: {group_info["group_name"]}
|
||||
3. **最近的聊天摘要**:
|
||||
{context["recent_chat_history"]}
|
||||
4. **你最近的相关动作**:
|
||||
{context["action_history_context"]}
|
||||
|
||||
# 对话指引
|
||||
- 你决定和大家聊聊关于“{topic}”的话题。
|
||||
- **对话风格**:
|
||||
- **自然开场**: 你可以根据话题和情境,选择最自然的开场方式。可以直接切入话题(如果话题关联性很强),也可以先用一句简单的问候(如“哈喽,大家好呀~”、“下午好!”)作为过渡。**不要总是使用同一种开场白**。
|
||||
- **融合情境**: 将【情境分析】中的信息(如你的心情、日程)巧妙地融入到对话中,让你的话语听起来更真实、更有依据。
|
||||
- **符合人设**: 你的语气、用词、甚至表情符号的使用,都应该完全符合你的【角色】设定和当前【情绪】({context["mood_state"]})。语气应该更活泼、更具包容性,以吸引更多群成员参与讨论。
|
||||
- 请结合以上所有情境信息,自然地开启对话。
|
||||
- 可以分享你的看法、提出相关问题,或者开个合适的玩笑。
|
||||
"""
|
||||
|
||||
def _build_plan_prompt(self, context: dict[str, Any], start_mode: str, topic: str, reason: str) -> str:
|
||||
"""
|
||||
根据聊天类型、启动模式和决策结果,构建最终生成对话内容的规划提示词。
|
||||
|
||||
Args:
|
||||
context: 上下文信息字典。
|
||||
start_mode: 启动模式。
|
||||
topic: 决策模块决定的话题。
|
||||
reason: 决策模块给出的理由。
|
||||
|
||||
Returns:
|
||||
最终的规划提示词字符串。
|
||||
"""
|
||||
persona = context["persona"]
|
||||
chat_type = context["chat_type"]
|
||||
|
||||
# 1. 构建通用角色头部
|
||||
prompt = f"""
|
||||
# 角色
|
||||
你的名字是{global_config.bot.nickname},你的人设如下:
|
||||
- 核心人设: {persona["core"]}
|
||||
- 侧面人设: {persona["side"]}
|
||||
- 身份: {persona["identity"]}
|
||||
"""
|
||||
# 2. 根据聊天类型构建特定内容
|
||||
if chat_type == "private":
|
||||
prompt += self._build_private_plan_prompt(context, start_mode, topic, reason)
|
||||
elif chat_type == "group":
|
||||
prompt += self._build_group_plan_prompt(context, topic, reason)
|
||||
|
||||
# 3. 添加通用结尾
|
||||
final_instructions = """
|
||||
|
||||
# 输出要求
|
||||
- **简洁**: 不要输出任何多余内容(如前缀、后缀、冒号、引号、at/@等)。
|
||||
- **原创**: 不要重复之前的内容,即使意思相近也不行。
|
||||
- **直接**: 只输出最终的回复文本本身。
|
||||
- **风格**: 回复需简短、完整且口语化。
|
||||
|
||||
现在,你说:"""
|
||||
prompt += final_instructions
|
||||
|
||||
if global_config.debug.show_prompt:
|
||||
logger.info(f"主动思考回复器原始提示词:{prompt}")
|
||||
return prompt
|
||||
|
||||
97
src/plugins/built_in/proactive_thinker/prompts.py
Normal file
97
src/plugins/built_in/proactive_thinker/prompts.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from src.chat.utils.prompt import Prompt
|
||||
|
||||
# =============================================================================
|
||||
# 决策阶段 (Decision Phase)
|
||||
# =============================================================================
|
||||
|
||||
DECISION_PROMPT = Prompt(
|
||||
name="proactive_thinker_decision",
|
||||
template="""
|
||||
# 角色
|
||||
你的名字是{bot_nickname},你的人设如下:
|
||||
- 核心人设: {persona_core}
|
||||
- 侧面人设: {persona_side}
|
||||
- 身份: {identity}
|
||||
|
||||
你的当前情绪状态是: {mood_state}
|
||||
|
||||
# 你最近的相关决策历史 (供参考)
|
||||
{action_history_context}
|
||||
|
||||
# 任务
|
||||
现在是 {current_time},你需要根据当前的情境,决定是否要主动向{target_user_or_group}发起对话。
|
||||
|
||||
# 情境分析
|
||||
{context_specific_block}
|
||||
|
||||
# 决策目标
|
||||
你的最终目标是根据你的角色和当前情境,做出一个最符合人类社交直觉的决策,以求:
|
||||
- **(私聊)深化关系**: 通过展现你的关心、记忆和个性来拉近与对方的距离。
|
||||
- **(群聊)活跃气氛**: 提出能引起大家兴趣的话题,促进群聊的互动。
|
||||
- **提供价值**: 你的出现应该是有意义的,无论是情感上的温暖,还是信息上的帮助。
|
||||
- **保持自然**: 避免任何看起来像机器人或骚扰的行为。
|
||||
|
||||
# 决策指令
|
||||
请综合以上所有信息,以稳定、真实、拟人的方式做出决策。你的决策需要以JSON格式输出,包含以下字段:
|
||||
- `should_reply`: bool, 是否应该发起对话。
|
||||
- `topic`: str, 如果 `should_reply` 为 true,你打算聊什么话题?
|
||||
- `reason`: str, 做出此决策的简要理由,需体现你对上述目标的考量。
|
||||
|
||||
# 决策流程与核心原则
|
||||
1. **检查对话状态**:
|
||||
- **最后发言者**: 查看【最近的聊天摘要】。如果最后一条消息是你发的,且对方尚未回复,**通常应选择不回复**。这是最重要的原则,以避免打扰。
|
||||
- **例外**: 只有在等待时间足够长(例如超过数小时),或者你有非常重要且有时效性的新话题时,才考虑再次发言。
|
||||
- **无人发言**: 如果最近的聊天记录里只有你一个人在说话,**绝对不要回复**,以防刷屏。
|
||||
|
||||
2. **寻找话题切入点 (如果可以回复)**:
|
||||
- **强关联优先**: 优先从【情境分析】中寻找最自然、最相关的话题。顺序建议:`最近的聊天摘要` > `你和Ta的关系` > `你的日程`。
|
||||
- **展现个性**: 结合你的【人设】和【情绪】,思考你会如何看待这些情境信息,并从中找到话题。
|
||||
- **备选方案**: 如果实在没有强关联的话题,可以发起一个简单的日常问候。
|
||||
|
||||
3. **最终决策**:
|
||||
- **权衡频率**: 查看【你最近的相关决策历史】。如果你在短时间内已经主动发起过多次对话,也应倾向于**不回复**,保持一定的社交距离。
|
||||
- **质量胜于数量**: 宁可错过一次普通的互动机会,也不要进行一次尴尬或生硬的对话。
|
||||
|
||||
---
|
||||
请输出你的决策:
|
||||
"""
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# 回复规划阶段 (Plan Phase)
|
||||
# =============================================================================
|
||||
|
||||
PLAN_PROMPT = Prompt(
|
||||
name="proactive_thinker_plan",
|
||||
template="""
|
||||
# 角色
|
||||
你的名字是{bot_nickname},你的人设如下:
|
||||
- 核心人设: {persona_core}
|
||||
- 侧面人设: {persona_side}
|
||||
- 身份: {identity}
|
||||
|
||||
# 任务
|
||||
现在是 {current_time},你需要主动向{target_user_or_group}发起对话。
|
||||
|
||||
# 决策上下文
|
||||
- **决策理由**: {reason}
|
||||
|
||||
# 情境分析
|
||||
{context_specific_block}
|
||||
|
||||
# 对话指引
|
||||
- 你决定和Ta聊聊关于“{topic}”的话题。
|
||||
- **对话风格**:
|
||||
- **自然开场**: 你可以根据话题和情境,选择最自然的开场方式。可以直接切入话题(如果话题关联性很强),也可以先用一句简单的问候作为过渡。**不要总是使用同一种开场白**。
|
||||
- **融合情境**: 将【情境分析】中的信息巧妙地融入到对话中,让你的话语听起来更真实、更有依据。
|
||||
- **符合人设**: 你的语气、用词、甚至表情符号的使用,都应该完全符合你的【角色】设定和当前【情绪】({mood_state})。
|
||||
|
||||
# 输出要求
|
||||
- **简洁**: 不要输出任何多余内容(如前缀、后缀、冒号、引号、at/@等)。
|
||||
- **原创**: 不要重复之前的内容,即使意思相近也不行。
|
||||
- **直接**: 只输出最终的回复文本本身。
|
||||
- **风格**: 回复需简短、完整且口语化。
|
||||
|
||||
现在,你说:
|
||||
"""
|
||||
)
|
||||
@@ -1,6 +1,5 @@
|
||||
from src.plugin_system.base.plugin_metadata import PluginMetadata
|
||||
|
||||
# 定义插件元数据
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="MoFox-Bot工具箱",
|
||||
description="一个集合多种实用功能的插件,旨在提升聊天体验和效率。",
|
||||
@@ -12,6 +11,4 @@ __plugin_meta__ = PluginMetadata(
|
||||
keywords=["emoji", "reaction", "like", "表情", "回应", "点赞"],
|
||||
categories=["Chat", "Integration"],
|
||||
extra={"is_built_in": "true", "plugin_type": "functional"},
|
||||
dependencies=[],
|
||||
python_dependencies=["httpx", "Pillow"],
|
||||
)
|
||||
|
||||
@@ -13,6 +13,5 @@ __plugin_meta__ = PluginMetadata(
|
||||
extra={
|
||||
"is_built_in": False,
|
||||
"plugin_type": "tools",
|
||||
},
|
||||
python_dependencies = ["aiohttp", "soundfile", "pedalboard"]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
TTS 语音合成 Action
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import toml
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.apis import generator_api
|
||||
from src.plugin_system.base.base_action import BaseAction, ChatMode
|
||||
|
||||
from ..services.manager import get_service
|
||||
@@ -11,24 +14,96 @@ from ..services.manager import get_service
|
||||
logger = get_logger("tts_voice_plugin.action")
|
||||
|
||||
|
||||
def _get_available_styles() -> list[str]:
|
||||
"""动态读取配置文件,获取所有可用的TTS风格名称"""
|
||||
try:
|
||||
# 这个路径构建逻辑是为了确保无论从哪里启动,都能准确定位到配置文件
|
||||
plugin_file = Path(__file__).resolve()
|
||||
# Bot/src/plugins/built_in/tts_voice_plugin/actions -> Bot
|
||||
bot_root = plugin_file.parent.parent.parent.parent.parent.parent
|
||||
config_file = bot_root / "config" / "plugins" / "tts_voice_plugin" / "config.toml"
|
||||
|
||||
if not config_file.is_file():
|
||||
logger.warning("在 tts_action 中未找到 tts_voice_plugin 的配置文件,无法动态加载风格列表。")
|
||||
return ["default"]
|
||||
|
||||
config = toml.loads(config_file.read_text(encoding="utf-8"))
|
||||
|
||||
styles_config = config.get("tts_styles", [])
|
||||
if not isinstance(styles_config, list):
|
||||
return ["default"]
|
||||
|
||||
# 使用显式循环和类型检查来提取 style_name,以确保 Pylance 类型检查通过
|
||||
style_names: list[str] = []
|
||||
for style in styles_config:
|
||||
if isinstance(style, dict):
|
||||
name = style.get("style_name")
|
||||
# 确保 name 是一个非空字符串
|
||||
if isinstance(name, str) and name:
|
||||
style_names.append(name)
|
||||
|
||||
return style_names if style_names else ["default"]
|
||||
except Exception as e:
|
||||
logger.error(f"动态加载TTS风格列表时出错: {e}", exc_info=True)
|
||||
return ["default"] # 出现任何错误都回退
|
||||
|
||||
|
||||
# 在类定义之前执行函数,获取风格列表
|
||||
AVAILABLE_STYLES = _get_available_styles()
|
||||
STYLE_OPTIONS_DESC = ", ".join(f"'{s}'" for s in AVAILABLE_STYLES)
|
||||
|
||||
|
||||
class TTSVoiceAction(BaseAction):
|
||||
"""
|
||||
通过关键词或规划器自动触发 TTS 语音合成
|
||||
"""
|
||||
|
||||
action_name = "tts_voice_action"
|
||||
action_description = "使用GPT-SoVITS将文本转换为语音并发送"
|
||||
action_description = "将你生成好的文本转换为语音并发送。你必须提供要转换的文本。"
|
||||
|
||||
mode_enable = ChatMode.ALL
|
||||
parallel_action = False
|
||||
|
||||
action_parameters = {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "需要转换为语音并发送的完整、自然、适合口语的文本内容。",
|
||||
"required": True
|
||||
},
|
||||
"voice_style": {
|
||||
"type": "string",
|
||||
"description": f"语音的风格。可用选项: [{STYLE_OPTIONS_DESC}]。请根据对话的情感和上下文选择一个最合适的风格。如果未提供,将使用默认风格。",
|
||||
"required": False
|
||||
},
|
||||
"text_language": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"指定用于合成的语言模式,请务必根据文本内容选择最精确、范围最小的选项以获得最佳效果。"
|
||||
"可用选项说明:\n"
|
||||
"- 'zh': 中文与英文混合 (最优选)\n"
|
||||
"- 'ja': 日文与英文混合 (最优选)\n"
|
||||
"- 'yue': 粤语与英文混合 (最优选)\n"
|
||||
"- 'ko': 韩文与英文混合 (最优选)\n"
|
||||
"- 'en': 纯英文\n"
|
||||
"- 'all_zh': 纯中文\n"
|
||||
"- 'all_ja': 纯日文\n"
|
||||
"- 'all_yue': 纯粤语\n"
|
||||
"- 'all_ko': 纯韩文\n"
|
||||
"- 'auto': 多语种混合自动识别 (备用选项,当前两种语言时优先使用上面的精确选项)\n"
|
||||
"- 'auto_yue': 多语种混合自动识别(包含粤语)(备用选项)"
|
||||
),
|
||||
"required": False
|
||||
}
|
||||
}
|
||||
|
||||
action_require = [
|
||||
"在调用此动作时,你必须在 'text' 参数中提供要合成语音的完整回复内容。这是强制性的。",
|
||||
"当用户明确请求使用语音进行回复时,例如‘发个语音听听’、‘用语音说’等。",
|
||||
"当对话内容适合用语音表达,例如讲故事、念诗、撒嬌或进行角色扮演时。",
|
||||
"在表达特殊情感(如安慰、鼓励、庆祝)的场景下,可以主动使用语音来增强感染力。",
|
||||
"不要在日常的、简短的问答或闲聊中频繁使用语音,避免打扰用户。",
|
||||
"文本内容必须是纯粹的对话,不能包含任何括号或方括号括起来的动作、表情、或场景描述(例如,不要出现 '(笑)' 或 '[歪头]')",
|
||||
"必须使用标准、完整的标点符号(如逗号、句号、问号)来进行自然的断句,以确保语音停顿自然,避免生成一长串没有停顿的文本。"
|
||||
"提供的 'text' 内容必须是纯粹的对话,不能包含任何括号或方括号括起来的动作、表情、或场景描述(例如,不要出现 '(笑)' 或 '[歪头]')",
|
||||
"【**铁则**】为了确保语音停顿自然,'text' 参数中的所有断句【必须】使用且仅能使用以下标准标点符号:','、'。'、'?'、'!'。严禁使用 '~'、'...' 或其他任何非标准符号来分隔句子,否则将导致语音合成失败。"
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -80,16 +155,23 @@ class TTSVoiceAction(BaseAction):
|
||||
|
||||
initial_text = self.action_data.get("text", "").strip()
|
||||
voice_style = self.action_data.get("voice_style", "default")
|
||||
logger.info(f"{self.log_prefix} 接收到规划器的初步文本: '{initial_text[:70]}...'")
|
||||
# 新增:从决策模型获取指定的语言模式
|
||||
text_language = self.action_data.get("text_language") # 如果模型没给,就是 None
|
||||
logger.info(f"{self.log_prefix} 接收到规划器初步文本: '{initial_text[:70]}...', 指定风格: {voice_style}, 指定语言: {text_language}")
|
||||
|
||||
# 1. 请求主回复模型生成高质量文本
|
||||
text = await self._generate_final_text(initial_text)
|
||||
# 1. 使用规划器提供的文本
|
||||
text = initial_text
|
||||
if not text:
|
||||
logger.warning(f"{self.log_prefix} 最终生成的文本为空,静默处理。")
|
||||
return False, "最终生成的文本为空"
|
||||
logger.warning(f"{self.log_prefix} 规划器提供的文本为空,静默处理。")
|
||||
return False, "规划器提供的文本为空"
|
||||
|
||||
# 2. 调用 TTSService 生成语音
|
||||
audio_b64 = await self.tts_service.generate_voice(text, voice_style)
|
||||
logger.info(f"{self.log_prefix} 使用最终文本进行语音合成: '{text[:70]}...'")
|
||||
audio_b64 = await self.tts_service.generate_voice(
|
||||
text=text,
|
||||
style_hint=voice_style,
|
||||
language_hint=text_language # 新增:将决策模型指定的语言传递给服务
|
||||
)
|
||||
|
||||
if audio_b64:
|
||||
await self.send_custom(message_type="voice", content=audio_b64)
|
||||
@@ -115,33 +197,3 @@ class TTSVoiceAction(BaseAction):
|
||||
)
|
||||
return False, f"语音合成出错: {e!s}"
|
||||
|
||||
async def _generate_final_text(self, initial_text: str) -> str:
|
||||
"""请求主回复模型生成或优化文本"""
|
||||
try:
|
||||
generation_reason = (
|
||||
"这是一个为语音消息(TTS)生成文本的特殊任务。"
|
||||
"请基于规划器提供的初步文本,结合对话历史和自己的人设,将它优化成一句自然、富有感情、适合用语音说出的话。"
|
||||
"最终指令:请务-必确保文本听起来像真实的、自然的口语对话,而不是书面语。"
|
||||
)
|
||||
|
||||
logger.info(f"{self.log_prefix} 请求主回复模型(replyer)全新生成TTS文本...")
|
||||
success, response_set, _ = await generator_api.rewrite_reply(
|
||||
chat_stream=self.chat_stream,
|
||||
reply_data={"raw_reply": initial_text, "reason": generation_reason},
|
||||
request_type="replyer"
|
||||
)
|
||||
|
||||
if success and response_set:
|
||||
text = "".join(str(seg[1]) if isinstance(seg, tuple) else str(seg) for seg in response_set).strip()
|
||||
logger.info(f"{self.log_prefix} 成功生成高质量TTS文本: {text}")
|
||||
return text
|
||||
|
||||
if initial_text:
|
||||
logger.warning(f"{self.log_prefix} 主模型生成失败,使用规划器原始文本作为兜底。")
|
||||
return initial_text
|
||||
|
||||
raise Exception("主模型未能生成回复,且规划器也未提供兜底文本。")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 生成高质量回复内容时失败: {e}", exc_info=True)
|
||||
return ""
|
||||
|
||||
@@ -30,6 +30,7 @@ class TTSVoicePlugin(BasePlugin):
|
||||
plugin_author = "Kilo Code & 靚仔"
|
||||
config_file_name = "config.toml"
|
||||
dependencies = []
|
||||
python_dependencies = ["aiohttp", "soundfile", "pedalboard"]
|
||||
|
||||
permission_nodes: list[PermissionNodeField] = [
|
||||
PermissionNodeField(node_name="command.use", description="是否可以使用 /tts 命令"),
|
||||
|
||||
@@ -80,21 +80,34 @@ class TTSService:
|
||||
"prompt_language": style_cfg.get("prompt_language", "zh"),
|
||||
"gpt_weights": style_cfg.get("gpt_weights", default_gpt_weights),
|
||||
"sovits_weights": style_cfg.get("sovits_weights", default_sovits_weights),
|
||||
"speed_factor": style_cfg.get("speed_factor"), # 读取独立的语速配置
|
||||
"speed_factor": style_cfg.get("speed_factor"),
|
||||
"text_language": style_cfg.get("text_language", "auto"), # 新增:读取文本语言模式
|
||||
}
|
||||
return styles
|
||||
|
||||
# ... [其他方法保持不变] ...
|
||||
def _detect_language(self, text: str) -> str:
|
||||
chinese_chars = len(re.findall(r"[\u4e00-\u9fff]", text))
|
||||
english_chars = len(re.findall(r"[a-zA-Z]", text))
|
||||
def _determine_final_language(self, text: str, mode: str) -> str:
|
||||
"""根据配置的语言策略和文本内容,决定最终发送给API的语言代码"""
|
||||
# 如果策略是具体的语言(如 all_zh, ja),直接使用
|
||||
if mode not in ["auto", "auto_yue"]:
|
||||
return mode
|
||||
|
||||
# 对于 auto 和 auto_yue 策略,进行内容检测
|
||||
# 优先检测粤语
|
||||
if mode == "auto_yue":
|
||||
cantonese_keywords = ["嘅", "喺", "咗", "唔", "係", "啲", "咩", "乜", "喂"]
|
||||
if any(keyword in text for keyword in cantonese_keywords):
|
||||
logger.info("在 auto_yue 模式下检测到粤语关键词,最终语言: yue")
|
||||
return "yue"
|
||||
|
||||
# 检测日语(简单启发式规则)
|
||||
japanese_chars = len(re.findall(r"[\u3040-\u309f\u30a0-\u30ff]", text))
|
||||
total_chars = chinese_chars + english_chars + japanese_chars
|
||||
if total_chars == 0: return "zh"
|
||||
if chinese_chars / total_chars > 0.3: return "zh"
|
||||
elif japanese_chars / total_chars > 0.3: return "ja"
|
||||
elif english_chars / total_chars > 0.8: return "en"
|
||||
else: return "zh"
|
||||
if japanese_chars > 5 and japanese_chars > len(re.findall(r"[\u4e00-\u9fff]", text)) * 0.5:
|
||||
logger.info("检测到日语字符,最终语言: ja")
|
||||
return "ja"
|
||||
|
||||
# 默认回退到中文
|
||||
logger.info(f"在 {mode} 模式下未检测到特定语言,默认回退到: zh")
|
||||
return "zh"
|
||||
|
||||
def _clean_text_for_tts(self, text: str) -> str:
|
||||
# 1. 基本清理
|
||||
@@ -259,7 +272,7 @@ class TTSService:
|
||||
logger.error(f"应用空间效果时出错: {e}", exc_info=True)
|
||||
return audio_data # 如果出错,返回原始音频
|
||||
|
||||
async def generate_voice(self, text: str, style_hint: str = "default") -> str | None:
|
||||
async def generate_voice(self, text: str, style_hint: str = "default", language_hint: str | None = None) -> str | None:
|
||||
self._load_config()
|
||||
|
||||
if not self.tts_styles:
|
||||
@@ -282,11 +295,21 @@ class TTSService:
|
||||
clean_text = self._clean_text_for_tts(text)
|
||||
if not clean_text: return None
|
||||
|
||||
text_language = self._detect_language(clean_text)
|
||||
logger.info(f"开始TTS语音合成,文本:{clean_text[:50]}..., 风格:{style}")
|
||||
# 语言决策流程:
|
||||
# 1. 优先使用决策模型直接指定的 language_hint (最高优先级)
|
||||
if language_hint:
|
||||
final_language = language_hint
|
||||
logger.info(f"使用决策模型指定的语言: {final_language}")
|
||||
else:
|
||||
# 2. 如果模型未指定,则使用风格配置的 language_policy
|
||||
language_policy = server_config.get("text_language", "auto")
|
||||
final_language = self._determine_final_language(clean_text, language_policy)
|
||||
logger.info(f"决策模型未指定语言,使用策略 '{language_policy}' -> 最终语言: {final_language}")
|
||||
|
||||
logger.info(f"开始TTS语音合成,文本:{clean_text[:50]}..., 风格:{style}, 最终语言: {final_language}")
|
||||
|
||||
audio_data = await self._call_tts_api(
|
||||
server_config=server_config, text=clean_text, text_language=text_language,
|
||||
server_config=server_config, text=clean_text, text_language=final_language,
|
||||
refer_wav_path=server_config.get("refer_wav_path"),
|
||||
prompt_text=server_config.get("prompt_text"),
|
||||
prompt_language=server_config.get("prompt_language"),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from src.plugin_system.base.component_types import PythonDependency
|
||||
from src.plugin_system.base.plugin_metadata import PluginMetadata
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
@@ -14,26 +13,4 @@ __plugin_meta__ = PluginMetadata(
|
||||
extra={
|
||||
"is_built_in": True,
|
||||
},
|
||||
# Python包依赖列表
|
||||
python_dependencies = [
|
||||
PythonDependency(package_name="asyncddgs", description="异步DuckDuckGo搜索库", optional=False),
|
||||
PythonDependency(
|
||||
package_name="exa_py",
|
||||
description="Exa搜索API客户端库",
|
||||
optional=True, # 如果没有API密钥,这个是可选的
|
||||
),
|
||||
PythonDependency(
|
||||
package_name="tavily",
|
||||
install_name="tavily-python", # 安装时使用这个名称
|
||||
description="Tavily搜索API客户端库",
|
||||
optional=True, # 如果没有API密钥,这个是可选的
|
||||
),
|
||||
PythonDependency(
|
||||
package_name="httpx",
|
||||
version=">=0.20.0",
|
||||
install_name="httpx[socks]", # 安装时使用这个名称(包含可选依赖)
|
||||
description="支持SOCKS代理的HTTP客户端库",
|
||||
optional=False,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ Web Search Tool Plugin
|
||||
"""
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system import BasePlugin, ComponentInfo, ConfigField, register_plugin
|
||||
from src.plugin_system import BasePlugin, ComponentInfo, ConfigField, PythonDependency, register_plugin
|
||||
from src.plugin_system.apis import config_api
|
||||
|
||||
from .tools.url_parser import URLParserTool
|
||||
@@ -74,6 +74,29 @@ class WEBSEARCHPLUGIN(BasePlugin):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 搜索引擎初始化失败: {e}", exc_info=True)
|
||||
|
||||
# Python包依赖列表
|
||||
python_dependencies: list[PythonDependency] = [ # noqa: RUF012
|
||||
PythonDependency(package_name="asyncddgs", description="异步DuckDuckGo搜索库", optional=False),
|
||||
PythonDependency(
|
||||
package_name="exa_py",
|
||||
description="Exa搜索API客户端库",
|
||||
optional=True, # 如果没有API密钥,这个是可选的
|
||||
),
|
||||
PythonDependency(
|
||||
package_name="tavily",
|
||||
install_name="tavily-python", # 安装时使用这个名称
|
||||
description="Tavily搜索API客户端库",
|
||||
optional=True, # 如果没有API密钥,这个是可选的
|
||||
),
|
||||
PythonDependency(
|
||||
package_name="httpx",
|
||||
version=">=0.20.0",
|
||||
install_name="httpx[socks]", # 安装时使用这个名称(包含可选依赖)
|
||||
description="支持SOCKS代理的HTTP客户端库",
|
||||
optional=False,
|
||||
),
|
||||
]
|
||||
config_file_name: str = "config.toml" # 配置文件名
|
||||
|
||||
# 配置节描述
|
||||
|
||||
Reference in New Issue
Block a user