From 4e0b49a5741f2e61e6179f7f47968e43176408d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Thu, 8 May 2025 07:55:56 +0000 Subject: [PATCH 01/22] add dev container config --- .devcontainer/devcontainer.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..39edf126 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,26 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python +{ + "name": "Python 3", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Configure tool-specific properties. + "customizations" : { + "jetbrains" : { + "backend" : "PyCharm" + } + }, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} From 337c942b1ee1a6a8557db15568923915055459b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Thu, 8 May 2025 18:31:41 +0000 Subject: [PATCH 02/22] update dev container config, add port forwarding --- .devcontainer/devcontainer.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 39edf126..2a4827d5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/python { - "name": "Python 3", + "name": "MaiBot-DevContainer", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", @@ -9,7 +9,9 @@ // "features": {}, // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], + "forwardPorts": [ + "8000:8000" + ], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "pip3 install --user -r requirements.txt", From 87c8cc91563daf3fdaa3d0026df6803d5e542d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Thu, 8 May 2025 18:57:02 +0000 Subject: [PATCH 03/22] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E5=AE=B9=E5=99=A8=E5=8A=9F=E8=83=BD=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=8C=E5=8C=85=E6=8B=ACtmux=E5=92=8CGitHub=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .devcontainer/devcontainer.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2a4827d5..bca235d2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,14 @@ "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, + "features": { + "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { + "packages": [ + "tmux" + ] + }, + "ghcr.io/devcontainers/features/github-cli:1": {} + }, // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [ From d00a28f9d3820399ad712d39ce7b8978dd25c587 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 9 May 2025 11:53:47 +0800 Subject: [PATCH 04/22] =?UTF-8?q?feat=EF=BC=9A=E4=BF=AE=E6=94=B9=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E8=B0=83=E7=94=A8=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- llm_tool_benchmark_results.json | 145 --------------- src/heart_flow/sub_mind.py | 265 ++++++++++++++-------------- src/plugins/utils/json_utils.py | 84 +++++++++ 从0.6.0升级0.6.3请先看我.txt | 1 - 4 files changed, 215 insertions(+), 280 deletions(-) delete mode 100644 llm_tool_benchmark_results.json delete mode 100644 从0.6.0升级0.6.3请先看我.txt diff --git a/llm_tool_benchmark_results.json b/llm_tool_benchmark_results.json deleted file mode 100644 index 6caa7c31..00000000 --- a/llm_tool_benchmark_results.json +++ /dev/null @@ -1,145 +0,0 @@ -{ - "测试时间": "2025-04-28 14:12:36", - "测试迭代次数": 10, - "不使用工具调用": { - "平均耗时": 4.596814393997192, - "最短耗时": 2.957131862640381, - "最长耗时": 10.121938705444336, - "标准差": 2.1705468730949593, - "所有耗时": [ - 3.18, - 4.65, - 10.12, - 3.5, - 4.46, - 4.24, - 3.23, - 6.2, - 2.96, - 3.42 - ] - }, - "不使用工具调用_详细响应": [ - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"怎么啦?\",\n \"emoji_query\": \"友好地询问\"\n}\n```", - "推理内容摘要": "" - }, - { - "内容摘要": "decide_reply_action(\n action=\"text_reply\",\n reasoning=\"千石连续两次叫了我的名字,显然是想引起我的注意或有事要说,作为礼貌应当回应\",\n emoji_query=\"友善的回应\"\n)", - "推理内容摘要": "" - }, - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"怎么啦?我在呢~\",\n \"emoji_query\": \"友好的询问\",\n \"reasoning\": \"由于对方连续两次提到我的名字,显然是想与我交流,应当及时给予友好回应避免冷场\"\n}\n```", - "推理内容摘要": "" - }, - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"在呢在呢~怎么啦?\",\n \"emoji_query\": \"好奇的回应\"\n}\n```", - "推理内容摘要": "" - }, - { - "内容摘要": "decide_reply_action(\n action=\"text_reply\",\n reasoning=\"千石连续两次提到我的名字,显然需要我回应。文字回复更正式且能明确表示我在关注他的信息。\",\n emoji_query=\"友好的回应\"\n)", - "推理内容摘要": "" - }, - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"reasoning\": \"千石连续两次提到我的名字,显然是需要我的回应。作为日常交流,应该给予友善简短的答复。\",\n \"emoji_query\": \"疑惑的歪头\"\n}\n```", - "推理内容摘要": "" - }, - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"咋啦千石~\",\n \"emoji_query\": \"好奇的询问\"\n}\n```", - "推理内容摘要": "" - }, - { - "内容摘要": "decide_reply_action\n```json\n{\n \"action\": \"text_reply\",\n \"content\": \"我在呢~怎么啦?\",\n \"emoji_query\": \"友好的关心\",\n \"reasoning\": \"千石连续两次呼唤我的名字,显然是有事情要找我或想引起我的注意。根据回复原则2(有人提到你但未回应),应该用友善的文字进行回应,并附上表达关心的表情符号来延...", - "推理内容摘要": "" - }, - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"千石怎么啦~\",\n \"emoji_query\": \"好奇的探询\"\n}\n```", - "推理内容摘要": "" - }, - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"怎么啦?突然叫我两次\",\n \"emoji_query\": \"好奇的疑问\"\n}\n```", - "推理内容摘要": "" - } - ], - "使用工具调用": { - "平均耗时": 8.139546775817871, - "最短耗时": 4.9980738162994385, - "最长耗时": 18.803313732147217, - "标准差": 4.008772720760647, - "所有耗时": [ - 5.81, - 18.8, - 6.06, - 8.06, - 10.07, - 6.34, - 7.9, - 6.66, - 5.0, - 6.69 - ] - }, - "使用工具调用_详细响应": [ - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - } - ], - "差异百分比": 77.07 -} \ No newline at end of file diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 1275fbbf..08ee406d 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -8,68 +8,99 @@ from src.individuality.individuality import Individuality import random from ..plugins.utils.prompt_builder import Prompt, global_prompt_manager from src.do_tool.tool_use import ToolUser -from src.plugins.utils.json_utils import safe_json_dumps, process_llm_tool_calls +from src.plugins.utils.json_utils import safe_json_dumps, safe_json_loads, process_llm_tool_calls, convert_custom_to_standard_tool_calls from src.heart_flow.chat_state_info import ChatStateInfo from src.plugins.chat.chat_stream import chat_manager from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo import difflib from src.plugins.person_info.relationship_manager import relationship_manager +import json logger = get_logger("sub_heartflow") def init_prompt(): - # --- Group Chat Prompt --- - group_prompt = """ -{extra_info} -{relation_prompt} -你的名字是{bot_name},{prompt_personality} -{last_loop_prompt} -{cycle_info_block} -现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容: -{chat_observe_info} + # 定义静态的工具调用JSON格式说明文本 + tool_json_format_instructions_text = """ +如果你需要使用工具,请按照以下JSON格式输出: +{ + "thinking": "你的内心思考过程,即使调用工具也需要这部分", + "tool_calls": [ // 这是一个工具调用对象的列表,如果不需要工具则此列表为空或省略此键 + { + "name": "工具的名称", // 例如:"get_memory" 或 "compare_numbers" + "arguments": { // 注意:这里 arguments 是一个 JSON 对象 + "参数名1": "参数值1", // 例如:"topic": "最近的聊天" + "参数名2": "参数值2" // 例如:"max_memories": 3 + // ... 根据工具定义的其他参数 + } + } + // ... 如果有更多工具调用的话 + ] +} +如果不需要使用工具,你可以只输出你的思考内容文本(此时不需要遵循JSON格式),或者输出一个仅包含 "thinking" 键的JSON对象。""" -你现在{mood_info} -请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。然后思考你是否需要使用函数工具。 -思考并输出你的内心想法 + # --- Group Chat Prompt --- + group_prompt_text = f""" +{{extra_info}} +{{relation_prompt}} +你的名字是{{bot_name}},{{prompt_personality}} +{{last_loop_prompt}} +{{cycle_info_block}} +现在是{{time_now}},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容: +{{chat_observe_info}} + +你现在{{mood_info}} +请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。 +然后思考你是否需要使用函数工具来帮助你获取信息或执行操作。 + +【可用的工具】 +如果你决定使用工具,你可以使用以下这些: +{{{{available_tools_info}}}} + +【工具使用格式】 +{tool_json_format_instructions_text} + +现在,请先输出你的内心想法。如果需要调用工具,请按照上述格式组织你的输出。 输出要求: -1. 根据聊天内容生成你的想法,{hf_do_next} -2. 不要分点、不要使用表情符号 -3. 避免多余符号(冒号、引号、括号等) -4. 语言简洁自然,不要浮夸 -5. 如果你刚发言,并且没有人回复你,不要回复 -工具使用说明: -1. 输出想法后考虑是否需要使用工具 -2. 工具可获取信息或执行操作 -3. 如需处理消息或回复,请使用工具。""" - Prompt(group_prompt, "sub_heartflow_prompt_before") +1. 根据聊天内容生成你的想法,{{hf_do_next}} +2. (想法部分)不要分点、不要使用表情符号 +3. (想法部分)避免多余符号(冒号、引号、括号等) +4. (想法部分)语言简洁自然,不要浮夸 +5. 如果你刚发言,并且没有人回复你,通常不需要回复,除非有特殊原因或需要使用工具获取新信息。 +""" + Prompt(group_prompt_text, "sub_heartflow_prompt_before") # --- Private Chat Prompt --- - private_prompt = """ -{extra_info} -{relation_prompt} -你的名字是{bot_name},{prompt_personality} -{last_loop_prompt} -{cycle_info_block} -现在是{time_now},你正在上网,和 {chat_target_name} 私聊,以下是你们的聊天内容: -{chat_observe_info} + private_prompt_text = f""" +{{extra_info}} +{{relation_prompt}} +你的名字是{{bot_name}},{{prompt_personality}} +{{last_loop_prompt}} +{{cycle_info_block}} +现在是{{time_now}},你正在上网,和 {{{{chat_target_name}}}} 私聊,以下是你们的聊天内容: +{{chat_observe_info}} -你现在{mood_info} -请仔细阅读聊天内容,想想你和 {chat_target_name} 的关系,回顾你们刚刚的交流,你刚刚发言和对方的反应,思考聊天的主题。 -请思考你要不要回复以及如何回复对方。然后思考你是否需要使用函数工具。 -思考并输出你的内心想法 +你现在{{mood_info}} +请仔细阅读聊天内容,想想你和 {{{{chat_target_name}}}} 的关系,回顾你们刚刚的交流,你刚刚发言和对方的反应,思考聊天的主题。 +请思考你要不要回复以及如何回复对方。然后思考你是否需要使用函数工具来帮助你获取信息或执行操作。 + +【可用的工具】 +如果你决定使用工具,你可以使用以下这些: +{{{{available_tools_info}}}} + +【工具使用格式】 +{tool_json_format_instructions_text} + +现在,请先输出你的内心想法。如果需要调用工具,请按照上述格式组织你的输出。 输出要求: -1. 根据聊天内容生成你的想法,{hf_do_next} -2. 不要分点、不要使用表情符号 -3. 避免多余符号(冒号、引号、括号等) -4. 语言简洁自然,不要浮夸 -5. 如果你刚发言,对方没有回复你,请谨慎回复 -工具使用说明: -1. 输出想法后考虑是否需要使用工具 -2. 工具可获取信息或执行操作 -3. 如需处理消息或回复,请使用工具。""" - Prompt(private_prompt, "sub_heartflow_prompt_private_before") # New template name +1. 根据聊天内容生成你的想法,{{hf_do_next}} +2. (想法部分)不要分点、不要使用表情符号 +3. (想法部分)避免多余符号(冒号、引号、括号等) +4. (想法部分)语言简洁自然,不要浮夸 +5. 如果你刚发言,对方没有回复你,请谨慎回复,除非有特殊原因或需要使用工具获取新信息。 +""" + Prompt(private_prompt_text, "sub_heartflow_prompt_private_before") # --- Last Loop Prompt (remains the same) --- last_loop_t = """ @@ -203,7 +234,7 @@ class SubMind: # 获取观察对象 observation: ChattingObservation = self.observations[0] if self.observations else None - if not observation or not hasattr(observation, "is_group_chat"): # Ensure it's ChattingObservation or similar + if not observation or not hasattr(observation, "is_group_chat"): logger.error(f"{self.log_prefix} 无法获取有效的观察对象或缺少聊天类型信息") self.update_current_mind("(观察出错了...)") return self.current_mind, self.past_mind @@ -217,7 +248,6 @@ class SubMind: chat_target_name = ( chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or chat_target_name ) - # --- End getting observation info --- # 获取观察内容 chat_observe_info = observation.get_observe_info() @@ -227,17 +257,19 @@ class SubMind: # 初始化工具 tool_instance = ToolUser() tools = tool_instance._define_tools() + # 生成工具信息字符串,用于填充到提示词模板中 + available_tools_info_str = "\n".join([f"- {tool['function']['name']}: {tool['function']['description']}" for tool in tools]) + if not available_tools_info_str: + available_tools_info_str = "当前没有可用的工具。" + # 获取个性化信息 individuality = Individuality.get_instance() relation_prompt = "" - # print(f"person_list: {person_list}") for person in person_list: relation_prompt += await relationship_manager.build_relationship_info(person, is_id=True) - # print(f"relat22222ion_prompt: {relation_prompt}") - # 构建个性部分 prompt_personality = individuality.get_prompt(x_person=2, level=2) @@ -304,7 +336,6 @@ class SubMind: formatted_response = "[空回复]" if not response_text else " ".join(response_text) responses_for_prompt.append(formatted_response) else: - # 一旦遇到非文本回复,连续性中断 break # 根据连续文本回复的数量构建提示信息 @@ -316,11 +347,9 @@ class SubMind: elif consecutive_text_replies == 1: # 如果最近的一个活动是文本回复 cycle_info_block = f'你刚刚已经回复一条消息(内容: "{responses_for_prompt[0]}")' - # 包装提示块,增加可读性,即使没有连续回复也给个标记 if cycle_info_block: cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n" else: - # 如果最近的活动循环不是文本回复,或者没有活动循环 cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n" # 加权随机选择思考指导 @@ -329,8 +358,7 @@ class SubMind: )[0] # ---------- 4. 构建最终提示词 ---------- - # --- Choose template based on chat type --- - logger.debug(f"is_group_chat: {is_group_chat}") + # 选择模板 if is_group_chat: template_name = "sub_heartflow_prompt_before" prompt = (await global_prompt_manager.get_prompt_async(template_name)).format( @@ -344,24 +372,24 @@ class SubMind: hf_do_next=hf_do_next, last_loop_prompt=last_loop_prompt, cycle_info_block=cycle_info_block, - # chat_target_name is not used in group prompt + available_tools_info=available_tools_info_str # 新增:填充可用工具列表 ) - else: # Private chat + else: template_name = "sub_heartflow_prompt_private_before" prompt = (await global_prompt_manager.get_prompt_async(template_name)).format( extra_info=self.structured_info_str, prompt_personality=prompt_personality, - relation_prompt=relation_prompt, # Might need adjustment for private context + relation_prompt=relation_prompt, bot_name=individuality.name, time_now=time_now, - chat_target_name=chat_target_name, # Pass target name + chat_target_name=chat_target_name, chat_observe_info=chat_observe_info, mood_info=mood_info, hf_do_next=hf_do_next, last_loop_prompt=last_loop_prompt, cycle_info_block=cycle_info_block, + available_tools_info=available_tools_info_str # 新增:填充可用工具列表 ) - # --- End choosing template --- # ---------- 5. 执行LLM请求并处理响应 ---------- content = "" # 初始化内容变量 @@ -369,34 +397,49 @@ class SubMind: try: # 调用LLM生成响应 - response, _reasoning_content, tool_calls = await self.llm_model.generate_response_tool_async( - prompt=prompt, tools=tools - ) + response, _reasoning_content, _ = await self.llm_model.generate_response(prompt=prompt) + + if not response: + logger.warning(f"{self.log_prefix} LLM返回空结果,思考失败。") + content = "(不知道该想些什么...)" + return self.current_mind, self.past_mind - logger.debug(f"{self.log_prefix} 子心流输出的原始LLM响应: {response}") + # 使用 safe_json_loads 解析响应 + parsed_response = safe_json_loads(response, default_value={}) + + if parsed_response: + thinking = parsed_response.get("thinking", "") + tool_calls_custom_format = parsed_response.get("tool_calls", []) # Renamed for clarity - # 直接使用LLM返回的文本响应作为 content - content = response if response else "" + # 处理工具调用 + if tool_calls_custom_format: + # 使用 convert_custom_to_standard_tool_calls 将自定义格式转换为标准格式 + success, valid_tool_calls_standard_format, error_msg = convert_custom_to_standard_tool_calls(tool_calls_custom_format, self.log_prefix) + + if success and valid_tool_calls_standard_format: + for tool_call_standard in valid_tool_calls_standard_format: # Iterate over standard format + try: + # _execute_tool_call expects the standard format + result = await tool_instance._execute_tool_call(tool_call_standard) + + if result: + new_item = { + "type": result.get("type", "unknown_type"), + "id": result.get("id", f"fallback_id_{time.time()}"), + "content": result.get("content", ""), + "ttl": 3, + } + self.structured_info.append(new_item) + except Exception as tool_e: + logger.error(f"{self.log_prefix} 工具执行失败: {tool_e}") + logger.error(traceback.format_exc()) + else: + logger.warning(f"{self.log_prefix} 自定义工具调用转换或验证失败: {error_msg}") - if tool_calls: - # 直接将 tool_calls 传递给处理函数 - success, valid_tool_calls, error_msg = process_llm_tool_calls( - tool_calls, log_prefix=f"{self.log_prefix} " - ) - - if success and valid_tool_calls: - # 记录工具调用信息 - tool_calls_str = ", ".join( - [call.get("function", {}).get("name", "未知工具") for call in valid_tool_calls] - ) - logger.info(f"{self.log_prefix} 模型请求调用{len(valid_tool_calls)}个工具: {tool_calls_str}") - - # 收集工具执行结果 - await self._execute_tool_calls(valid_tool_calls, tool_instance) - elif not success: - logger.warning(f"{self.log_prefix} 处理工具调用时出错: {error_msg}") + content = thinking else: - logger.info(f"{self.log_prefix} 心流未使用工具") + # 如果不是JSON格式或解析失败,直接使用响应内容 + content = response except Exception as e: # 处理总体异常 @@ -404,22 +447,14 @@ class SubMind: logger.error(traceback.format_exc()) content = "思考过程中出现错误" - # 记录初步思考结果 - logger.debug(f"{self.log_prefix} 初步心流思考结果: {content}\nprompt: {prompt}\n") - - # 处理空响应情况 - if not content: - content = "(不知道该想些什么...)" - logger.warning(f"{self.log_prefix} LLM返回空结果,思考失败。") - # ---------- 6. 应用概率性去重和修饰 ---------- - new_content = content # 保存 LLM 直接输出的结果 + new_content = content try: similarity = calculate_similarity(previous_mind, new_content) replacement_prob = calculate_replacement_probability(similarity) logger.debug(f"{self.log_prefix} 新旧想法相似度: {similarity:.2f}, 替换概率: {replacement_prob:.2f}") - # 定义词语列表 (移到判断之前) + # 定义词语列表 yu_qi_ci_liebiao = ["嗯", "哦", "啊", "唉", "哈", "唔"] zhuan_zhe_liebiao = ["但是", "不过", "然而", "可是", "只是"] cheng_jie_liebiao = ["然后", "接着", "此外", "而且", "另外"] @@ -477,58 +512,20 @@ class SubMind: content = new_content # 保留原始 content else: logger.debug(f"{self.log_prefix} 未执行概率性去重 (概率: {replacement_prob:.2f})") - # content 保持 new_content 不变 except Exception as e: logger.error(f"{self.log_prefix} 应用概率性去重或特殊处理时出错: {e}") logger.error(traceback.format_exc()) - # 出错时保留原始 content - content = new_content + content = new_content # 出错时保留去重前的内容,或者可以考虑保留原始LLM输出 # ---------- 7. 更新思考状态并返回结果 ---------- logger.info(f"{self.log_prefix} 最终心流思考结果: {content}") # 更新当前思考内容 self.update_current_mind(content) + self._update_structured_info_str() return self.current_mind, self.past_mind - async def _execute_tool_calls(self, tool_calls, tool_instance): - """ - 执行一组工具调用并收集结果 - - 参数: - tool_calls: 工具调用列表 - tool_instance: 工具使用器实例 - """ - tool_results = [] - new_structured_items = [] # 收集新产生的结构化信息 - - # 执行所有工具调用 - for tool_call in tool_calls: - try: - result = await tool_instance._execute_tool_call(tool_call) - if result: - tool_results.append(result) - # 创建新的结构化信息项 - new_item = { - "type": result.get("type", "unknown_type"), # 使用 'type' 键 - "id": result.get("id", f"fallback_id_{time.time()}"), # 使用 'id' 键 - "content": result.get("content", ""), # 'content' 键保持不变 - "ttl": 3, - } - new_structured_items.append(new_item) - - except Exception as tool_e: - logger.error(f"[{self.subheartflow_id}] 工具执行失败: {tool_e}") - logger.error(traceback.format_exc()) # 添加 traceback 记录 - - # 如果有新的工具结果,记录并更新结构化信息 - if new_structured_items: - self.structured_info.extend(new_structured_items) # 添加到现有列表 - logger.debug(f"工具调用收集到新的结构化信息: {safe_json_dumps(new_structured_items, ensure_ascii=False)}") - # logger.debug(f"当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}") # 可以取消注释以查看完整列表 - self._update_structured_info_str() # 添加新信息后,更新字符串表示 - def update_current_mind(self, response): if self.current_mind: # 只有当 current_mind 非空时才添加到 past_mind self.past_mind.append(self.current_mind) diff --git a/src/plugins/utils/json_utils.py b/src/plugins/utils/json_utils.py index 6226e6e9..0040aa4f 100644 --- a/src/plugins/utils/json_utils.py +++ b/src/plugins/utils/json_utils.py @@ -2,6 +2,7 @@ import json import logging from typing import Any, Dict, TypeVar, List, Union, Tuple import ast +import time # 定义类型变量用于泛型类型提示 T = TypeVar("T") @@ -224,3 +225,86 @@ def process_llm_tool_calls( return False, [], "所有工具调用格式均无效" return True, valid_tool_calls, "" + + +def convert_custom_to_standard_tool_calls( + custom_tool_calls: List[Dict[str, Any]], log_prefix: str = "" +) -> Tuple[bool, List[Dict[str, Any]], str]: + """ + Converts a list of tool calls from a custom format (name, arguments_object) + to a standard format (id, type, function_with_name_and_arguments_string). + + Custom format per item: + { + "name": "tool_name", + "arguments": {"param": "value"} + } + + Standard format per item: + { + "id": "call_...", + "type": "function", + "function": { + "name": "tool_name", + "arguments": "{\"param\": \"value\"}" + } + } + + Args: + custom_tool_calls: List of tool calls in the custom format. + log_prefix: Logger prefix. + + Returns: + Tuple (success_flag, list_of_standard_tool_calls, error_message) + """ + if not isinstance(custom_tool_calls, list): + return False, [], f"{log_prefix}Input custom_tool_calls is not a list, got {type(custom_tool_calls).__name__}" + + if not custom_tool_calls: + return True, [], "Custom tool call list is empty." + + standard_tool_calls = [] + for i, custom_call in enumerate(custom_tool_calls): + if not isinstance(custom_call, dict): + msg = f"{log_prefix}Item {i} in custom_tool_calls is not a dictionary, got {type(custom_call).__name__}" + logger.warning(msg) + return False, [], msg + + tool_name = custom_call.get("name") + tool_arguments_obj = custom_call.get("arguments") + + if not isinstance(tool_name, str) or not tool_name: + msg = f"{log_prefix}Item {i} ('{custom_call}') is missing 'name' or name is not a string." + logger.warning(msg) + return False, [], msg + + if not isinstance(tool_arguments_obj, dict): + # Allow empty arguments if it's missing, defaulting to {} + if tool_arguments_obj is None: + tool_arguments_obj = {} + else: + msg = f"{log_prefix}Item {i} ('{tool_name}') has 'arguments' but it's not a dictionary, got {type(tool_arguments_obj).__name__}." + logger.warning(msg) + return False, [], msg + + arguments_str = safe_json_dumps(tool_arguments_obj, default_value="{}", ensure_ascii=False) + if arguments_str == "{}" and tool_arguments_obj: # safe_json_dumps failed for non-empty obj + msg = f"{log_prefix}Item {i} ('{tool_name}') failed to dump arguments to JSON string: {tool_arguments_obj}" + logger.warning(msg) + # Potentially return False here if strict dumping is required, or proceed with "{}" + # For now, we'll proceed with "{}" if dumping fails but log it. + + standard_call_id = f"call_{int(time.time() * 1000)}_{i}" + + standard_call = { + "id": standard_call_id, + "type": "function", + "function": { + "name": tool_name, + "arguments": arguments_str, + }, + } + standard_tool_calls.append(standard_call) + + logger.debug(f"{log_prefix}Converted {len(custom_tool_calls)} custom calls to {len(standard_tool_calls)} standard calls.") + return True, standard_tool_calls, "" diff --git a/从0.6.0升级0.6.3请先看我.txt b/从0.6.0升级0.6.3请先看我.txt deleted file mode 100644 index 734061c9..00000000 --- a/从0.6.0升级0.6.3请先看我.txt +++ /dev/null @@ -1 +0,0 @@ -该版本变动了人格相关设置,原有的配置内容可能被自动更新,如果你没有备份,可以在\config\old找回 \ No newline at end of file From 8e910748c7d3227fd65a0c60bc1fdfa04ecd568e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Fri, 9 May 2025 04:36:10 +0000 Subject: [PATCH 05/22] add postCreateCommand in devcontainer.json --- .devcontainer/devcontainer.json | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bca235d2..beefc9e6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,11 +1,6 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/python { "name": "MaiBot-DevContainer", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", - - // Features to add to the dev container. More info: https://containers.dev/features. "features": { "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { "packages": [ @@ -14,22 +9,13 @@ }, "ghcr.io/devcontainers/features/github-cli:1": {} }, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [ "8000:8000" ], - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "pip3 install --user -r requirements.txt", - - // Configure tool-specific properties. + "postCreateCommand": "pip3 install --user -r requirements.txt", "customizations" : { "jetbrains" : { "backend" : "PyCharm" } - }, - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + } } From 36544c6f81b02ad915f86949d4ea2a78e63b32e3 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 9 May 2025 13:30:09 +0800 Subject: [PATCH 06/22] =?UTF-8?q?fix=EF=BC=9A=E6=81=92=E5=AE=9A=E6=8F=90?= =?UTF-8?q?=E5=8F=96memory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/do_tool/{tool_can_use => not_used}/get_memory.py | 0 src/plugins/heartFC_chat/heartFC_chat.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/do_tool/{tool_can_use => not_used}/get_memory.py (100%) diff --git a/src/do_tool/tool_can_use/get_memory.py b/src/do_tool/not_used/get_memory.py similarity index 100% rename from src/do_tool/tool_can_use/get_memory.py rename to src/do_tool/not_used/get_memory.py diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 28c17d9a..b594bf02 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -777,7 +777,7 @@ class HeartFChatting: ) return current_mind except Exception as e: - logger.error(f"{self.log_prefix}[SubMind] 思考失败: {e}") + logger.error(f"{self.log_prefix}子心流 思考失败: {e}") logger.error(traceback.format_exc()) return "[思考时出错]" From c5e8d508d5fdf705bb627eaf9b75a442ded7df83 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 9 May 2025 13:33:26 +0800 Subject: [PATCH 07/22] =?UTF-8?q?Revert=20"feat=EF=BC=9A=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E6=A8=A1=E5=BC=8F"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d00a28f9d3820399ad712d39ce7b8978dd25c587. --- llm_tool_benchmark_results.json | 145 +++++++++++++++ src/heart_flow/sub_mind.py | 263 ++++++++++++++-------------- src/plugins/utils/json_utils.py | 84 --------- 从0.6.0升级0.6.3请先看我.txt | 1 + 4 files changed, 279 insertions(+), 214 deletions(-) create mode 100644 llm_tool_benchmark_results.json create mode 100644 从0.6.0升级0.6.3请先看我.txt diff --git a/llm_tool_benchmark_results.json b/llm_tool_benchmark_results.json new file mode 100644 index 00000000..6caa7c31 --- /dev/null +++ b/llm_tool_benchmark_results.json @@ -0,0 +1,145 @@ +{ + "测试时间": "2025-04-28 14:12:36", + "测试迭代次数": 10, + "不使用工具调用": { + "平均耗时": 4.596814393997192, + "最短耗时": 2.957131862640381, + "最长耗时": 10.121938705444336, + "标准差": 2.1705468730949593, + "所有耗时": [ + 3.18, + 4.65, + 10.12, + 3.5, + 4.46, + 4.24, + 3.23, + 6.2, + 2.96, + 3.42 + ] + }, + "不使用工具调用_详细响应": [ + { + "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"怎么啦?\",\n \"emoji_query\": \"友好地询问\"\n}\n```", + "推理内容摘要": "" + }, + { + "内容摘要": "decide_reply_action(\n action=\"text_reply\",\n reasoning=\"千石连续两次叫了我的名字,显然是想引起我的注意或有事要说,作为礼貌应当回应\",\n emoji_query=\"友善的回应\"\n)", + "推理内容摘要": "" + }, + { + "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"怎么啦?我在呢~\",\n \"emoji_query\": \"友好的询问\",\n \"reasoning\": \"由于对方连续两次提到我的名字,显然是想与我交流,应当及时给予友好回应避免冷场\"\n}\n```", + "推理内容摘要": "" + }, + { + "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"在呢在呢~怎么啦?\",\n \"emoji_query\": \"好奇的回应\"\n}\n```", + "推理内容摘要": "" + }, + { + "内容摘要": "decide_reply_action(\n action=\"text_reply\",\n reasoning=\"千石连续两次提到我的名字,显然需要我回应。文字回复更正式且能明确表示我在关注他的信息。\",\n emoji_query=\"友好的回应\"\n)", + "推理内容摘要": "" + }, + { + "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"reasoning\": \"千石连续两次提到我的名字,显然是需要我的回应。作为日常交流,应该给予友善简短的答复。\",\n \"emoji_query\": \"疑惑的歪头\"\n}\n```", + "推理内容摘要": "" + }, + { + "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"咋啦千石~\",\n \"emoji_query\": \"好奇的询问\"\n}\n```", + "推理内容摘要": "" + }, + { + "内容摘要": "decide_reply_action\n```json\n{\n \"action\": \"text_reply\",\n \"content\": \"我在呢~怎么啦?\",\n \"emoji_query\": \"友好的关心\",\n \"reasoning\": \"千石连续两次呼唤我的名字,显然是有事情要找我或想引起我的注意。根据回复原则2(有人提到你但未回应),应该用友善的文字进行回应,并附上表达关心的表情符号来延...", + "推理内容摘要": "" + }, + { + "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"千石怎么啦~\",\n \"emoji_query\": \"好奇的探询\"\n}\n```", + "推理内容摘要": "" + }, + { + "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"怎么啦?突然叫我两次\",\n \"emoji_query\": \"好奇的疑问\"\n}\n```", + "推理内容摘要": "" + } + ], + "使用工具调用": { + "平均耗时": 8.139546775817871, + "最短耗时": 4.9980738162994385, + "最长耗时": 18.803313732147217, + "标准差": 4.008772720760647, + "所有耗时": [ + 5.81, + 18.8, + 6.06, + 8.06, + 10.07, + 6.34, + 7.9, + 6.66, + 5.0, + 6.69 + ] + }, + "使用工具调用_详细响应": [ + { + "内容摘要": "", + "推理内容摘要": "", + "工具调用数量": 0, + "工具调用详情": [] + }, + { + "内容摘要": "", + "推理内容摘要": "", + "工具调用数量": 0, + "工具调用详情": [] + }, + { + "内容摘要": "", + "推理内容摘要": "", + "工具调用数量": 0, + "工具调用详情": [] + }, + { + "内容摘要": "", + "推理内容摘要": "", + "工具调用数量": 0, + "工具调用详情": [] + }, + { + "内容摘要": "", + "推理内容摘要": "", + "工具调用数量": 0, + "工具调用详情": [] + }, + { + "内容摘要": "", + "推理内容摘要": "", + "工具调用数量": 0, + "工具调用详情": [] + }, + { + "内容摘要": "", + "推理内容摘要": "", + "工具调用数量": 0, + "工具调用详情": [] + }, + { + "内容摘要": "", + "推理内容摘要": "", + "工具调用数量": 0, + "工具调用详情": [] + }, + { + "内容摘要": "", + "推理内容摘要": "", + "工具调用数量": 0, + "工具调用详情": [] + }, + { + "内容摘要": "", + "推理内容摘要": "", + "工具调用数量": 0, + "工具调用详情": [] + } + ], + "差异百分比": 77.07 +} \ No newline at end of file diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 08ee406d..1275fbbf 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -8,99 +8,68 @@ from src.individuality.individuality import Individuality import random from ..plugins.utils.prompt_builder import Prompt, global_prompt_manager from src.do_tool.tool_use import ToolUser -from src.plugins.utils.json_utils import safe_json_dumps, safe_json_loads, process_llm_tool_calls, convert_custom_to_standard_tool_calls +from src.plugins.utils.json_utils import safe_json_dumps, process_llm_tool_calls from src.heart_flow.chat_state_info import ChatStateInfo from src.plugins.chat.chat_stream import chat_manager from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo import difflib from src.plugins.person_info.relationship_manager import relationship_manager -import json logger = get_logger("sub_heartflow") def init_prompt(): - # 定义静态的工具调用JSON格式说明文本 - tool_json_format_instructions_text = """ -如果你需要使用工具,请按照以下JSON格式输出: -{ - "thinking": "你的内心思考过程,即使调用工具也需要这部分", - "tool_calls": [ // 这是一个工具调用对象的列表,如果不需要工具则此列表为空或省略此键 - { - "name": "工具的名称", // 例如:"get_memory" 或 "compare_numbers" - "arguments": { // 注意:这里 arguments 是一个 JSON 对象 - "参数名1": "参数值1", // 例如:"topic": "最近的聊天" - "参数名2": "参数值2" // 例如:"max_memories": 3 - // ... 根据工具定义的其他参数 - } - } - // ... 如果有更多工具调用的话 - ] -} -如果不需要使用工具,你可以只输出你的思考内容文本(此时不需要遵循JSON格式),或者输出一个仅包含 "thinking" 键的JSON对象。""" - # --- Group Chat Prompt --- - group_prompt_text = f""" -{{extra_info}} -{{relation_prompt}} -你的名字是{{bot_name}},{{prompt_personality}} -{{last_loop_prompt}} -{{cycle_info_block}} -现在是{{time_now}},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容: -{{chat_observe_info}} + group_prompt = """ +{extra_info} +{relation_prompt} +你的名字是{bot_name},{prompt_personality} +{last_loop_prompt} +{cycle_info_block} +现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容: +{chat_observe_info} -你现在{{mood_info}} -请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。 -然后思考你是否需要使用函数工具来帮助你获取信息或执行操作。 - -【可用的工具】 -如果你决定使用工具,你可以使用以下这些: -{{{{available_tools_info}}}} - -【工具使用格式】 -{tool_json_format_instructions_text} - -现在,请先输出你的内心想法。如果需要调用工具,请按照上述格式组织你的输出。 +你现在{mood_info} +请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。然后思考你是否需要使用函数工具。 +思考并输出你的内心想法 输出要求: -1. 根据聊天内容生成你的想法,{{hf_do_next}} -2. (想法部分)不要分点、不要使用表情符号 -3. (想法部分)避免多余符号(冒号、引号、括号等) -4. (想法部分)语言简洁自然,不要浮夸 -5. 如果你刚发言,并且没有人回复你,通常不需要回复,除非有特殊原因或需要使用工具获取新信息。 -""" - Prompt(group_prompt_text, "sub_heartflow_prompt_before") +1. 根据聊天内容生成你的想法,{hf_do_next} +2. 不要分点、不要使用表情符号 +3. 避免多余符号(冒号、引号、括号等) +4. 语言简洁自然,不要浮夸 +5. 如果你刚发言,并且没有人回复你,不要回复 +工具使用说明: +1. 输出想法后考虑是否需要使用工具 +2. 工具可获取信息或执行操作 +3. 如需处理消息或回复,请使用工具。""" + Prompt(group_prompt, "sub_heartflow_prompt_before") # --- Private Chat Prompt --- - private_prompt_text = f""" -{{extra_info}} -{{relation_prompt}} -你的名字是{{bot_name}},{{prompt_personality}} -{{last_loop_prompt}} -{{cycle_info_block}} -现在是{{time_now}},你正在上网,和 {{{{chat_target_name}}}} 私聊,以下是你们的聊天内容: -{{chat_observe_info}} + private_prompt = """ +{extra_info} +{relation_prompt} +你的名字是{bot_name},{prompt_personality} +{last_loop_prompt} +{cycle_info_block} +现在是{time_now},你正在上网,和 {chat_target_name} 私聊,以下是你们的聊天内容: +{chat_observe_info} -你现在{{mood_info}} -请仔细阅读聊天内容,想想你和 {{{{chat_target_name}}}} 的关系,回顾你们刚刚的交流,你刚刚发言和对方的反应,思考聊天的主题。 -请思考你要不要回复以及如何回复对方。然后思考你是否需要使用函数工具来帮助你获取信息或执行操作。 - -【可用的工具】 -如果你决定使用工具,你可以使用以下这些: -{{{{available_tools_info}}}} - -【工具使用格式】 -{tool_json_format_instructions_text} - -现在,请先输出你的内心想法。如果需要调用工具,请按照上述格式组织你的输出。 +你现在{mood_info} +请仔细阅读聊天内容,想想你和 {chat_target_name} 的关系,回顾你们刚刚的交流,你刚刚发言和对方的反应,思考聊天的主题。 +请思考你要不要回复以及如何回复对方。然后思考你是否需要使用函数工具。 +思考并输出你的内心想法 输出要求: -1. 根据聊天内容生成你的想法,{{hf_do_next}} -2. (想法部分)不要分点、不要使用表情符号 -3. (想法部分)避免多余符号(冒号、引号、括号等) -4. (想法部分)语言简洁自然,不要浮夸 -5. 如果你刚发言,对方没有回复你,请谨慎回复,除非有特殊原因或需要使用工具获取新信息。 -""" - Prompt(private_prompt_text, "sub_heartflow_prompt_private_before") +1. 根据聊天内容生成你的想法,{hf_do_next} +2. 不要分点、不要使用表情符号 +3. 避免多余符号(冒号、引号、括号等) +4. 语言简洁自然,不要浮夸 +5. 如果你刚发言,对方没有回复你,请谨慎回复 +工具使用说明: +1. 输出想法后考虑是否需要使用工具 +2. 工具可获取信息或执行操作 +3. 如需处理消息或回复,请使用工具。""" + Prompt(private_prompt, "sub_heartflow_prompt_private_before") # New template name # --- Last Loop Prompt (remains the same) --- last_loop_t = """ @@ -234,7 +203,7 @@ class SubMind: # 获取观察对象 observation: ChattingObservation = self.observations[0] if self.observations else None - if not observation or not hasattr(observation, "is_group_chat"): + if not observation or not hasattr(observation, "is_group_chat"): # Ensure it's ChattingObservation or similar logger.error(f"{self.log_prefix} 无法获取有效的观察对象或缺少聊天类型信息") self.update_current_mind("(观察出错了...)") return self.current_mind, self.past_mind @@ -248,6 +217,7 @@ class SubMind: chat_target_name = ( chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or chat_target_name ) + # --- End getting observation info --- # 获取观察内容 chat_observe_info = observation.get_observe_info() @@ -257,19 +227,17 @@ class SubMind: # 初始化工具 tool_instance = ToolUser() tools = tool_instance._define_tools() - # 生成工具信息字符串,用于填充到提示词模板中 - available_tools_info_str = "\n".join([f"- {tool['function']['name']}: {tool['function']['description']}" for tool in tools]) - if not available_tools_info_str: - available_tools_info_str = "当前没有可用的工具。" - # 获取个性化信息 individuality = Individuality.get_instance() relation_prompt = "" + # print(f"person_list: {person_list}") for person in person_list: relation_prompt += await relationship_manager.build_relationship_info(person, is_id=True) + # print(f"relat22222ion_prompt: {relation_prompt}") + # 构建个性部分 prompt_personality = individuality.get_prompt(x_person=2, level=2) @@ -336,6 +304,7 @@ class SubMind: formatted_response = "[空回复]" if not response_text else " ".join(response_text) responses_for_prompt.append(formatted_response) else: + # 一旦遇到非文本回复,连续性中断 break # 根据连续文本回复的数量构建提示信息 @@ -347,9 +316,11 @@ class SubMind: elif consecutive_text_replies == 1: # 如果最近的一个活动是文本回复 cycle_info_block = f'你刚刚已经回复一条消息(内容: "{responses_for_prompt[0]}")' + # 包装提示块,增加可读性,即使没有连续回复也给个标记 if cycle_info_block: cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n" else: + # 如果最近的活动循环不是文本回复,或者没有活动循环 cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n" # 加权随机选择思考指导 @@ -358,7 +329,8 @@ class SubMind: )[0] # ---------- 4. 构建最终提示词 ---------- - # 选择模板 + # --- Choose template based on chat type --- + logger.debug(f"is_group_chat: {is_group_chat}") if is_group_chat: template_name = "sub_heartflow_prompt_before" prompt = (await global_prompt_manager.get_prompt_async(template_name)).format( @@ -372,24 +344,24 @@ class SubMind: hf_do_next=hf_do_next, last_loop_prompt=last_loop_prompt, cycle_info_block=cycle_info_block, - available_tools_info=available_tools_info_str # 新增:填充可用工具列表 + # chat_target_name is not used in group prompt ) - else: + else: # Private chat template_name = "sub_heartflow_prompt_private_before" prompt = (await global_prompt_manager.get_prompt_async(template_name)).format( extra_info=self.structured_info_str, prompt_personality=prompt_personality, - relation_prompt=relation_prompt, + relation_prompt=relation_prompt, # Might need adjustment for private context bot_name=individuality.name, time_now=time_now, - chat_target_name=chat_target_name, + chat_target_name=chat_target_name, # Pass target name chat_observe_info=chat_observe_info, mood_info=mood_info, hf_do_next=hf_do_next, last_loop_prompt=last_loop_prompt, cycle_info_block=cycle_info_block, - available_tools_info=available_tools_info_str # 新增:填充可用工具列表 ) + # --- End choosing template --- # ---------- 5. 执行LLM请求并处理响应 ---------- content = "" # 初始化内容变量 @@ -397,49 +369,34 @@ class SubMind: try: # 调用LLM生成响应 - response, _reasoning_content, _ = await self.llm_model.generate_response(prompt=prompt) - - if not response: - logger.warning(f"{self.log_prefix} LLM返回空结果,思考失败。") - content = "(不知道该想些什么...)" - return self.current_mind, self.past_mind + response, _reasoning_content, tool_calls = await self.llm_model.generate_response_tool_async( + prompt=prompt, tools=tools + ) - # 使用 safe_json_loads 解析响应 - parsed_response = safe_json_loads(response, default_value={}) - - if parsed_response: - thinking = parsed_response.get("thinking", "") - tool_calls_custom_format = parsed_response.get("tool_calls", []) # Renamed for clarity + logger.debug(f"{self.log_prefix} 子心流输出的原始LLM响应: {response}") - # 处理工具调用 - if tool_calls_custom_format: - # 使用 convert_custom_to_standard_tool_calls 将自定义格式转换为标准格式 - success, valid_tool_calls_standard_format, error_msg = convert_custom_to_standard_tool_calls(tool_calls_custom_format, self.log_prefix) - - if success and valid_tool_calls_standard_format: - for tool_call_standard in valid_tool_calls_standard_format: # Iterate over standard format - try: - # _execute_tool_call expects the standard format - result = await tool_instance._execute_tool_call(tool_call_standard) - - if result: - new_item = { - "type": result.get("type", "unknown_type"), - "id": result.get("id", f"fallback_id_{time.time()}"), - "content": result.get("content", ""), - "ttl": 3, - } - self.structured_info.append(new_item) - except Exception as tool_e: - logger.error(f"{self.log_prefix} 工具执行失败: {tool_e}") - logger.error(traceback.format_exc()) - else: - logger.warning(f"{self.log_prefix} 自定义工具调用转换或验证失败: {error_msg}") + # 直接使用LLM返回的文本响应作为 content + content = response if response else "" - content = thinking + if tool_calls: + # 直接将 tool_calls 传递给处理函数 + success, valid_tool_calls, error_msg = process_llm_tool_calls( + tool_calls, log_prefix=f"{self.log_prefix} " + ) + + if success and valid_tool_calls: + # 记录工具调用信息 + tool_calls_str = ", ".join( + [call.get("function", {}).get("name", "未知工具") for call in valid_tool_calls] + ) + logger.info(f"{self.log_prefix} 模型请求调用{len(valid_tool_calls)}个工具: {tool_calls_str}") + + # 收集工具执行结果 + await self._execute_tool_calls(valid_tool_calls, tool_instance) + elif not success: + logger.warning(f"{self.log_prefix} 处理工具调用时出错: {error_msg}") else: - # 如果不是JSON格式或解析失败,直接使用响应内容 - content = response + logger.info(f"{self.log_prefix} 心流未使用工具") except Exception as e: # 处理总体异常 @@ -447,14 +404,22 @@ class SubMind: logger.error(traceback.format_exc()) content = "思考过程中出现错误" + # 记录初步思考结果 + logger.debug(f"{self.log_prefix} 初步心流思考结果: {content}\nprompt: {prompt}\n") + + # 处理空响应情况 + if not content: + content = "(不知道该想些什么...)" + logger.warning(f"{self.log_prefix} LLM返回空结果,思考失败。") + # ---------- 6. 应用概率性去重和修饰 ---------- - new_content = content + new_content = content # 保存 LLM 直接输出的结果 try: similarity = calculate_similarity(previous_mind, new_content) replacement_prob = calculate_replacement_probability(similarity) logger.debug(f"{self.log_prefix} 新旧想法相似度: {similarity:.2f}, 替换概率: {replacement_prob:.2f}") - # 定义词语列表 + # 定义词语列表 (移到判断之前) yu_qi_ci_liebiao = ["嗯", "哦", "啊", "唉", "哈", "唔"] zhuan_zhe_liebiao = ["但是", "不过", "然而", "可是", "只是"] cheng_jie_liebiao = ["然后", "接着", "此外", "而且", "另外"] @@ -512,20 +477,58 @@ class SubMind: content = new_content # 保留原始 content else: logger.debug(f"{self.log_prefix} 未执行概率性去重 (概率: {replacement_prob:.2f})") + # content 保持 new_content 不变 except Exception as e: logger.error(f"{self.log_prefix} 应用概率性去重或特殊处理时出错: {e}") logger.error(traceback.format_exc()) - content = new_content # 出错时保留去重前的内容,或者可以考虑保留原始LLM输出 + # 出错时保留原始 content + content = new_content # ---------- 7. 更新思考状态并返回结果 ---------- logger.info(f"{self.log_prefix} 最终心流思考结果: {content}") # 更新当前思考内容 self.update_current_mind(content) - self._update_structured_info_str() return self.current_mind, self.past_mind + async def _execute_tool_calls(self, tool_calls, tool_instance): + """ + 执行一组工具调用并收集结果 + + 参数: + tool_calls: 工具调用列表 + tool_instance: 工具使用器实例 + """ + tool_results = [] + new_structured_items = [] # 收集新产生的结构化信息 + + # 执行所有工具调用 + for tool_call in tool_calls: + try: + result = await tool_instance._execute_tool_call(tool_call) + if result: + tool_results.append(result) + # 创建新的结构化信息项 + new_item = { + "type": result.get("type", "unknown_type"), # 使用 'type' 键 + "id": result.get("id", f"fallback_id_{time.time()}"), # 使用 'id' 键 + "content": result.get("content", ""), # 'content' 键保持不变 + "ttl": 3, + } + new_structured_items.append(new_item) + + except Exception as tool_e: + logger.error(f"[{self.subheartflow_id}] 工具执行失败: {tool_e}") + logger.error(traceback.format_exc()) # 添加 traceback 记录 + + # 如果有新的工具结果,记录并更新结构化信息 + if new_structured_items: + self.structured_info.extend(new_structured_items) # 添加到现有列表 + logger.debug(f"工具调用收集到新的结构化信息: {safe_json_dumps(new_structured_items, ensure_ascii=False)}") + # logger.debug(f"当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}") # 可以取消注释以查看完整列表 + self._update_structured_info_str() # 添加新信息后,更新字符串表示 + def update_current_mind(self, response): if self.current_mind: # 只有当 current_mind 非空时才添加到 past_mind self.past_mind.append(self.current_mind) diff --git a/src/plugins/utils/json_utils.py b/src/plugins/utils/json_utils.py index 0040aa4f..6226e6e9 100644 --- a/src/plugins/utils/json_utils.py +++ b/src/plugins/utils/json_utils.py @@ -2,7 +2,6 @@ import json import logging from typing import Any, Dict, TypeVar, List, Union, Tuple import ast -import time # 定义类型变量用于泛型类型提示 T = TypeVar("T") @@ -225,86 +224,3 @@ def process_llm_tool_calls( return False, [], "所有工具调用格式均无效" return True, valid_tool_calls, "" - - -def convert_custom_to_standard_tool_calls( - custom_tool_calls: List[Dict[str, Any]], log_prefix: str = "" -) -> Tuple[bool, List[Dict[str, Any]], str]: - """ - Converts a list of tool calls from a custom format (name, arguments_object) - to a standard format (id, type, function_with_name_and_arguments_string). - - Custom format per item: - { - "name": "tool_name", - "arguments": {"param": "value"} - } - - Standard format per item: - { - "id": "call_...", - "type": "function", - "function": { - "name": "tool_name", - "arguments": "{\"param\": \"value\"}" - } - } - - Args: - custom_tool_calls: List of tool calls in the custom format. - log_prefix: Logger prefix. - - Returns: - Tuple (success_flag, list_of_standard_tool_calls, error_message) - """ - if not isinstance(custom_tool_calls, list): - return False, [], f"{log_prefix}Input custom_tool_calls is not a list, got {type(custom_tool_calls).__name__}" - - if not custom_tool_calls: - return True, [], "Custom tool call list is empty." - - standard_tool_calls = [] - for i, custom_call in enumerate(custom_tool_calls): - if not isinstance(custom_call, dict): - msg = f"{log_prefix}Item {i} in custom_tool_calls is not a dictionary, got {type(custom_call).__name__}" - logger.warning(msg) - return False, [], msg - - tool_name = custom_call.get("name") - tool_arguments_obj = custom_call.get("arguments") - - if not isinstance(tool_name, str) or not tool_name: - msg = f"{log_prefix}Item {i} ('{custom_call}') is missing 'name' or name is not a string." - logger.warning(msg) - return False, [], msg - - if not isinstance(tool_arguments_obj, dict): - # Allow empty arguments if it's missing, defaulting to {} - if tool_arguments_obj is None: - tool_arguments_obj = {} - else: - msg = f"{log_prefix}Item {i} ('{tool_name}') has 'arguments' but it's not a dictionary, got {type(tool_arguments_obj).__name__}." - logger.warning(msg) - return False, [], msg - - arguments_str = safe_json_dumps(tool_arguments_obj, default_value="{}", ensure_ascii=False) - if arguments_str == "{}" and tool_arguments_obj: # safe_json_dumps failed for non-empty obj - msg = f"{log_prefix}Item {i} ('{tool_name}') failed to dump arguments to JSON string: {tool_arguments_obj}" - logger.warning(msg) - # Potentially return False here if strict dumping is required, or proceed with "{}" - # For now, we'll proceed with "{}" if dumping fails but log it. - - standard_call_id = f"call_{int(time.time() * 1000)}_{i}" - - standard_call = { - "id": standard_call_id, - "type": "function", - "function": { - "name": tool_name, - "arguments": arguments_str, - }, - } - standard_tool_calls.append(standard_call) - - logger.debug(f"{log_prefix}Converted {len(custom_tool_calls)} custom calls to {len(standard_tool_calls)} standard calls.") - return True, standard_tool_calls, "" diff --git a/从0.6.0升级0.6.3请先看我.txt b/从0.6.0升级0.6.3请先看我.txt new file mode 100644 index 00000000..734061c9 --- /dev/null +++ b/从0.6.0升级0.6.3请先看我.txt @@ -0,0 +1 @@ +该版本变动了人格相关设置,原有的配置内容可能被自动更新,如果你没有备份,可以在\config\old找回 \ No newline at end of file From 501840dcdd626f3bf4ffb9991fae8deb7a8f8167 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 9 May 2025 13:33:37 +0800 Subject: [PATCH 08/22] =?UTF-8?q?Revert=20"feat=EF=BC=9A=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E6=A8=A1=E5=BC=8F"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d00a28f9d3820399ad712d39ce7b8978dd25c587. --- src/heart_flow/mai_state_manager.py | 4 +- template/bot_config_meta.toml | 134 ++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 template/bot_config_meta.toml diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py index d289a94a..71e2abbe 100644 --- a/src/heart_flow/mai_state_manager.py +++ b/src/heart_flow/mai_state_manager.py @@ -13,8 +13,8 @@ logger = get_logger("mai_state") # 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 = False +enable_unlimited_hfc_chat = True # 调试用:无限专注聊天 +# enable_unlimited_hfc_chat = False prevent_offline_state = True # 目前默认不启用OFFLINE状态 diff --git a/template/bot_config_meta.toml b/template/bot_config_meta.toml new file mode 100644 index 00000000..459b7026 --- /dev/null +++ b/template/bot_config_meta.toml @@ -0,0 +1,134 @@ +[inner.version] +describe = "版本号" +important = true +can_edit = false + +[bot.qq] +describe = "机器人的QQ号" +important = true +can_edit = true + +[bot.nickname] +describe = "机器人的昵称" +important = true +can_edit = true + +[bot.alias_names] +describe = "机器人的别名列表,该选项还在调试中,暂时未生效" +important = false +can_edit = true + +[groups.talk_allowed] +describe = "可以回复消息的群号码列表" +important = true +can_edit = true + +[groups.talk_frequency_down] +describe = "降低回复频率的群号码列表" +important = false +can_edit = true + +[groups.ban_user_id] +describe = "禁止回复和读取消息的QQ号列表" +important = false +can_edit = true + +[personality.personality_core] +describe = "用一句话或几句话描述人格的核心特点,建议20字以内" +important = true +can_edit = true + +[personality.personality_sides] +describe = "用一句话或几句话描述人格的一些细节,条数任意,不能为0,该选项还在调试中" +important = false +can_edit = true + +[identity.identity_detail] +describe = "身份特点列表,条数任意,不能为0,该选项还在调试中" +important = false +can_edit = true + +[identity.age] +describe = "年龄,单位岁" +important = false +can_edit = true + +[identity.gender] +describe = "性别" +important = false +can_edit = true + +[identity.appearance] +describe = "外貌特征描述,该选项还在调试中,暂时未生效" +important = false +can_edit = true + +[schedule.enable_schedule_gen] +describe = "是否启用日程表" +important = false +can_edit = true + +[schedule.enable_schedule_interaction] +describe = "日程表是否影响回复模式" +important = false +can_edit = true + +[schedule.prompt_schedule_gen] +describe = "用几句话描述描述性格特点或行动规律,这个特征会用来生成日程表" +important = false +can_edit = true + +[schedule.schedule_doing_update_interval] +describe = "日程表更新间隔,单位秒" +important = false +can_edit = true + +[schedule.schedule_temperature] +describe = "日程表温度,建议0.1-0.5" +important = false +can_edit = true + +[schedule.time_zone] +describe = "时区设置,可以解决运行电脑时区和国内时区不同的情况,或者模拟国外留学生日程" +important = false +can_edit = true + +[platforms.nonebot-qq] +describe = "nonebot-qq适配器提供的链接" +important = true +can_edit = true + +[chat.allow_focus_mode] +describe = "是否允许专注聊天状态" +important = false +can_edit = true + +[chat.base_normal_chat_num] +describe = "最多允许多少个群进行普通聊天" +important = false +can_edit = true + +[chat.base_focused_chat_num] +describe = "最多允许多少个群进行专注聊天" +important = false +can_edit = true + +[chat.observation_context_size] +describe = "观察到的最长上下文大小,建议15,太短太长都会导致脑袋尖尖" +important = false +can_edit = true + +[chat.message_buffer] +describe = "启用消息缓冲器,启用此项以解决消息的拆分问题,但会使麦麦的回复延迟" +important = false +can_edit = true + +[chat.ban_words] +describe = "需要过滤的消息列表" +important = false +can_edit = true + +[chat.ban_msgs_regex] +describe = "需要过滤的消息(原始消息)匹配的正则表达式,匹配到的消息将被过滤(支持CQ码)" +important = false +can_edit = true \ No newline at end of file From 683dda7db2c69c436179f5855bfd65080f31ee5e Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 9 May 2025 13:34:13 +0800 Subject: [PATCH 09/22] fix --- src/heart_flow/mai_state_manager.py | 4 ++-- 从0.6.0升级0.6.3请先看我.txt | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 从0.6.0升级0.6.3请先看我.txt diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py index 71e2abbe..d289a94a 100644 --- a/src/heart_flow/mai_state_manager.py +++ b/src/heart_flow/mai_state_manager.py @@ -13,8 +13,8 @@ logger = get_logger("mai_state") # 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 = False +# enable_unlimited_hfc_chat = True # 调试用:无限专注聊天 +enable_unlimited_hfc_chat = False prevent_offline_state = True # 目前默认不启用OFFLINE状态 diff --git a/从0.6.0升级0.6.3请先看我.txt b/从0.6.0升级0.6.3请先看我.txt deleted file mode 100644 index 734061c9..00000000 --- a/从0.6.0升级0.6.3请先看我.txt +++ /dev/null @@ -1 +0,0 @@ -该版本变动了人格相关设置,原有的配置内容可能被自动更新,如果你没有备份,可以在\config\old找回 \ No newline at end of file From af2596d4312fe8946adba93be8001f95ffee9302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Fri, 9 May 2025 15:08:19 +0900 Subject: [PATCH 10/22] add extensions in devcontainer.json --- .devcontainer/devcontainer.json | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index beefc9e6..e61a92e3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,13 +9,23 @@ }, "ghcr.io/devcontainers/features/github-cli:1": {} }, - "forwardPorts": [ - "8000:8000" - ], + "forwardPorts": [ + "8000:8000" + ], "postCreateCommand": "pip3 install --user -r requirements.txt", - "customizations" : { - "jetbrains" : { - "backend" : "PyCharm" - } - } + "customizations": { + "jetbrains": { + "backend": "PyCharm" + }, + "vscode": { + "extensions": [ + "tamasfe.even-better-toml", + "njpwerner.autodocstring", + "ms-python.python", + "KevinRose.vsc-python-indent", + "ms-python.vscode-pylance", + "ms-python.autopep8" + ] + } + } } From ebdf32e247ac392acec8e5960a725fa4e4fa923a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 9 May 2025 19:34:40 +0800 Subject: [PATCH 11/22] =?UTF-8?q?doc:=20=E6=9B=B4=E6=96=B0=E9=9A=90?= =?UTF-8?q?=E7=A7=81=E6=9D=A1=E6=AC=BE=E5=92=8CEULA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EULA.md | 4 ++-- PRIVACY.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/EULA.md b/EULA.md index c7a734a2..20c6022f 100644 --- a/EULA.md +++ b/EULA.md @@ -1,8 +1,8 @@ # **MaiMBot最终用户许可协议** **版本:V1.0** -**更新日期:2025年3月18日** +**更新日期:2025年5月9日** **生效日期:2025年3月18日** -**适用的MaiMBot版本号:<=v0.5.15** +**适用的MaiMBot版本号:所有版本** **2025© MaiMBot项目团队** diff --git a/PRIVACY.md b/PRIVACY.md index ba85f617..c3c0e1b8 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,8 +1,8 @@ ### MaiMBot用户隐私条款 **版本:V1.0** -**更新日期:2025年3月18日** +**更新日期:2025年5月9日** **生效日期:2025年3月18日** -**适用的MaiMBot版本号:<=v0.5.15** +**适用的MaiMBot版本号:所有版本** **2025© MaiMBot项目团队** From 807e50eb4c29c91f742179ef64e0d842485bba0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 9 May 2025 19:37:37 +0800 Subject: [PATCH 12/22] doc: update x2 --- EULA.md | 4 ++-- PRIVACY.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/EULA.md b/EULA.md index 20c6022f..e21591a5 100644 --- a/EULA.md +++ b/EULA.md @@ -1,10 +1,10 @@ -# **MaiMBot最终用户许可协议** +# **MaiBot最终用户许可协议** **版本:V1.0** **更新日期:2025年5月9日** **生效日期:2025年3月18日** **适用的MaiMBot版本号:所有版本** -**2025© MaiMBot项目团队** +**2025© MaiBot项目团队** --- diff --git a/PRIVACY.md b/PRIVACY.md index c3c0e1b8..4e34a2c3 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,10 +1,10 @@ -### MaiMBot用户隐私条款 +### MaiBot用户隐私条款 **版本:V1.0** **更新日期:2025年5月9日** **生效日期:2025年3月18日** -**适用的MaiMBot版本号:所有版本** +**适用的MaiBot版本号:所有版本** -**2025© MaiMBot项目团队** +**2025© MaiBot项目团队** MaiMBot项目团队(以下简称项目团队)**尊重并保护**用户(以下简称您)的隐私。若您选择使用MaiMBot项目(以下简称本项目),则您需同意本项目按照以下隐私条款处理您的输入和输出内容: From 35b8d8ad0c295c9d6af0785826ed46b030268e3d Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 9 May 2025 21:00:52 +0800 Subject: [PATCH 13/22] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E6=94=B9=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=92=8C=E5=B0=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- llm_tool_benchmark_results.json | 145 ---------------------- src/config/config.py | 2 +- src/heart_flow/sub_heartflow.py | 32 +++-- src/heart_flow/sub_mind.py | 63 +++++++++- src/heart_flow/subheartflow_manager.py | 155 ++++++++---------------- src/plugins/heartFC_chat/normal_chat.py | 2 +- 6 files changed, 130 insertions(+), 269 deletions(-) delete mode 100644 llm_tool_benchmark_results.json diff --git a/llm_tool_benchmark_results.json b/llm_tool_benchmark_results.json deleted file mode 100644 index 6caa7c31..00000000 --- a/llm_tool_benchmark_results.json +++ /dev/null @@ -1,145 +0,0 @@ -{ - "测试时间": "2025-04-28 14:12:36", - "测试迭代次数": 10, - "不使用工具调用": { - "平均耗时": 4.596814393997192, - "最短耗时": 2.957131862640381, - "最长耗时": 10.121938705444336, - "标准差": 2.1705468730949593, - "所有耗时": [ - 3.18, - 4.65, - 10.12, - 3.5, - 4.46, - 4.24, - 3.23, - 6.2, - 2.96, - 3.42 - ] - }, - "不使用工具调用_详细响应": [ - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"怎么啦?\",\n \"emoji_query\": \"友好地询问\"\n}\n```", - "推理内容摘要": "" - }, - { - "内容摘要": "decide_reply_action(\n action=\"text_reply\",\n reasoning=\"千石连续两次叫了我的名字,显然是想引起我的注意或有事要说,作为礼貌应当回应\",\n emoji_query=\"友善的回应\"\n)", - "推理内容摘要": "" - }, - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"怎么啦?我在呢~\",\n \"emoji_query\": \"友好的询问\",\n \"reasoning\": \"由于对方连续两次提到我的名字,显然是想与我交流,应当及时给予友好回应避免冷场\"\n}\n```", - "推理内容摘要": "" - }, - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"在呢在呢~怎么啦?\",\n \"emoji_query\": \"好奇的回应\"\n}\n```", - "推理内容摘要": "" - }, - { - "内容摘要": "decide_reply_action(\n action=\"text_reply\",\n reasoning=\"千石连续两次提到我的名字,显然需要我回应。文字回复更正式且能明确表示我在关注他的信息。\",\n emoji_query=\"友好的回应\"\n)", - "推理内容摘要": "" - }, - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"reasoning\": \"千石连续两次提到我的名字,显然是需要我的回应。作为日常交流,应该给予友善简短的答复。\",\n \"emoji_query\": \"疑惑的歪头\"\n}\n```", - "推理内容摘要": "" - }, - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"咋啦千石~\",\n \"emoji_query\": \"好奇的询问\"\n}\n```", - "推理内容摘要": "" - }, - { - "内容摘要": "decide_reply_action\n```json\n{\n \"action\": \"text_reply\",\n \"content\": \"我在呢~怎么啦?\",\n \"emoji_query\": \"友好的关心\",\n \"reasoning\": \"千石连续两次呼唤我的名字,显然是有事情要找我或想引起我的注意。根据回复原则2(有人提到你但未回应),应该用友善的文字进行回应,并附上表达关心的表情符号来延...", - "推理内容摘要": "" - }, - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"千石怎么啦~\",\n \"emoji_query\": \"好奇的探询\"\n}\n```", - "推理内容摘要": "" - }, - { - "内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"怎么啦?突然叫我两次\",\n \"emoji_query\": \"好奇的疑问\"\n}\n```", - "推理内容摘要": "" - } - ], - "使用工具调用": { - "平均耗时": 8.139546775817871, - "最短耗时": 4.9980738162994385, - "最长耗时": 18.803313732147217, - "标准差": 4.008772720760647, - "所有耗时": [ - 5.81, - 18.8, - 6.06, - 8.06, - 10.07, - 6.34, - 7.9, - 6.66, - 5.0, - 6.69 - ] - }, - "使用工具调用_详细响应": [ - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - }, - { - "内容摘要": "", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - } - ], - "差异百分比": 77.07 -} \ No newline at end of file diff --git a/src/config/config.py b/src/config/config.py index 28d947ef..5c2bdcc2 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -25,7 +25,7 @@ logger = get_logger("config") # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 is_test = False mai_version_main = "0.6.3" -mai_version_fix = "fix-2" +mai_version_fix = "fix-3" if mai_version_fix: if is_test: diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 66d50762..de26d4f6 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -110,7 +110,7 @@ class SubHeartflow: logger.error(f"{self.log_prefix} 停止 NormalChat 监控任务时出错: {e}") logger.error(traceback.format_exc()) - async def _start_normal_chat(self) -> bool: + async def _start_normal_chat(self, rewind = False) -> bool: """ 启动 NormalChat 实例,并进行异步初始化。 进入 CHAT 状态时使用。 @@ -125,8 +125,11 @@ class SubHeartflow: if not chat_stream: logger.error(f"{log_prefix} 无法获取 chat_stream,无法启动 NormalChat。") return False - - self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict()) + if rewind: + self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict()) + else: + + self.normal_chat_instance = NormalChat(chat_stream=chat_stream) # 进行异步初始化 await self.normal_chat_instance.initialize() @@ -218,13 +221,22 @@ class SubHeartflow: if new_state == ChatState.CHAT: # 移除限额检查逻辑 logger.debug(f"{log_prefix} 准备进入或保持 聊天 状态") - if await self._start_normal_chat(): - # logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。") - state_changed = True + if current_state == ChatState.FOCUSED: + if await self._start_normal_chat(rewind=False): + # logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。") + state_changed = True + else: + logger.error(f"{log_prefix} 从FOCUSED状态启动 NormalChat 失败,无法进入 CHAT 状态。") + # 考虑是否需要回滚状态或采取其他措施 + return # 启动失败,不改变状态 else: - logger.error(f"{log_prefix} 启动 NormalChat 失败,无法进入 CHAT 状态。") - # 考虑是否需要回滚状态或采取其他措施 - return # 启动失败,不改变状态 + if await self._start_normal_chat(rewind=True): + # logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。") + state_changed = True + else: + logger.error(f"{log_prefix} 从ABSENT状态启动 NormalChat 失败,无法进入 CHAT 状态。") + # 考虑是否需要回滚状态或采取其他措施 + return # 启动失败,不改变状态 elif new_state == ChatState.FOCUSED: # 移除限额检查逻辑 @@ -239,6 +251,8 @@ class SubHeartflow: elif new_state == ChatState.ABSENT: logger.info(f"{log_prefix} 进入 ABSENT 状态,停止所有聊天活动...") + await self.clear_interest_dict() + await self._stop_normal_chat() await self._stop_heart_fc_chat() state_changed = True # 总是可以成功转换到 ABSENT diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 1275fbbf..b240f744 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -14,6 +14,8 @@ from src.plugins.chat.chat_stream import chat_manager from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo import difflib from src.plugins.person_info.relationship_manager import relationship_manager +from src.plugins.memory_system.Hippocampus import HippocampusManager +import jieba logger = get_logger("sub_heartflow") @@ -223,7 +225,56 @@ class SubMind: chat_observe_info = observation.get_observe_info() person_list = observation.person_list - # ---------- 2. 准备工具和个性化数据 ---------- + # ---------- 2. 获取记忆 ---------- + try: + # 从聊天内容中提取关键词 + chat_words = set(jieba.cut(chat_observe_info)) + # 过滤掉停用词和单字词 + keywords = [word for word in chat_words if len(word) > 1] + # 去重并限制数量 + keywords = list(set(keywords))[:5] + + logger.debug(f"{self.log_prefix} 提取的关键词: {keywords}") + # 检查已有记忆,过滤掉已存在的主题 + existing_topics = set() + for item in self.structured_info: + if item["type"] == "memory": + existing_topics.add(item["id"]) + + # 过滤掉已存在的主题 + filtered_keywords = [k for k in keywords if k not in existing_topics] + + if not filtered_keywords: + logger.debug(f"{self.log_prefix} 所有关键词对应的记忆都已存在,跳过记忆提取") + else: + # 调用记忆系统获取相关记忆 + related_memory = await HippocampusManager.get_instance().get_memory_from_topic( + valid_keywords=filtered_keywords, + max_memory_num=3, + max_memory_length=2, + max_depth=3 + ) + + logger.debug(f"{self.log_prefix} 获取到的记忆: {related_memory}") + + if related_memory: + for topic, memory in related_memory: + new_item = { + "type": "memory", + "id": topic, + "content": memory, + "ttl": 3 + } + self.structured_info.append(new_item) + logger.debug(f"{self.log_prefix} 添加新记忆: {topic} - {memory}") + else: + logger.debug(f"{self.log_prefix} 没有找到相关记忆") + + except Exception as e: + logger.error(f"{self.log_prefix} 获取记忆时出错: {e}") + logger.error(traceback.format_exc()) + + # ---------- 3. 准备工具和个性化数据 ---------- # 初始化工具 tool_instance = ToolUser() tools = tool_instance._define_tools() @@ -244,7 +295,7 @@ class SubMind: # 获取当前时间 time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) - # ---------- 3. 构建思考指导部分 ---------- + # ---------- 4. 构建思考指导部分 ---------- # 创建本地随机数生成器,基于分钟数作为种子 local_random = random.Random() current_minute = int(time.strftime("%M")) @@ -328,7 +379,7 @@ class SubMind: [option[0] for option in hf_options], weights=[option[1] for option in hf_options], k=1 )[0] - # ---------- 4. 构建最终提示词 ---------- + # ---------- 5. 构建最终提示词 ---------- # --- Choose template based on chat type --- logger.debug(f"is_group_chat: {is_group_chat}") if is_group_chat: @@ -363,7 +414,7 @@ class SubMind: ) # --- End choosing template --- - # ---------- 5. 执行LLM请求并处理响应 ---------- + # ---------- 6. 执行LLM请求并处理响应 ---------- content = "" # 初始化内容变量 _reasoning_content = "" # 初始化推理内容变量 @@ -412,7 +463,7 @@ class SubMind: content = "(不知道该想些什么...)" logger.warning(f"{self.log_prefix} LLM返回空结果,思考失败。") - # ---------- 6. 应用概率性去重和修饰 ---------- + # ---------- 7. 应用概率性去重和修饰 ---------- new_content = content # 保存 LLM 直接输出的结果 try: similarity = calculate_similarity(previous_mind, new_content) @@ -485,7 +536,7 @@ class SubMind: # 出错时保留原始 content content = new_content - # ---------- 7. 更新思考状态并返回结果 ---------- + # ---------- 8. 更新思考状态并返回结果 ---------- logger.info(f"{self.log_prefix} 最终心流思考结果: {content}") # 更新当前思考内容 self.update_current_mind(content) diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index f25594b7..ba23c70d 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -274,19 +274,17 @@ class SubHeartflowManager: async def sbhf_absent_into_focus(self): """评估子心流兴趣度,满足条件且未达上限则提升到FOCUSED状态(基于start_hfc_probability)""" try: - log_prefix = "[兴趣评估]" - # 使用 self.mai_state_info 获取当前状态和限制 current_state = self.mai_state_info.get_current_state() focused_limit = current_state.get_focused_chat_max_num() # --- 新增:检查是否允许进入 FOCUS 模式 --- # if not global_config.allow_focus_mode: if int(time.time()) % 60 == 0: # 每60秒输出一次日志避免刷屏 - logger.debug(f"{log_prefix} 配置不允许进入 FOCUSED 状态 (allow_focus_mode=False)") + logger.trace(f"未开启 FOCUSED 状态 (allow_focus_mode=False)") return # 如果不允许,直接返回 # --- 结束新增 --- - logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 可以在{focused_limit}个群激情聊天") + logger.info(f"当前状态 ({current_state.value}) 可以在{focused_limit}个群 专注聊天") if focused_limit <= 0: # logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 不允许 FOCUSED 子心流") @@ -294,35 +292,36 @@ class SubHeartflowManager: current_focused_count = self.count_subflows_by_state(ChatState.FOCUSED) if current_focused_count >= focused_limit: - logger.debug(f"{log_prefix} 已达专注上限 ({current_focused_count}/{focused_limit})") + logger.debug(f"已达专注上限 ({current_focused_count}/{focused_limit})") return for sub_hf in list(self.subheartflows.values()): flow_id = sub_hf.subheartflow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id - - logger.debug(f"{log_prefix} 检查子心流: {stream_name},现在状态: {sub_hf.chat_state.chat_status.value}") - + # 跳过非CHAT状态或已经是FOCUSED状态的子心流 if sub_hf.chat_state.chat_status == ChatState.FOCUSED: continue + + + if sub_hf.interest_chatting.start_hfc_probability == 0: + continue + else: + logger.debug(f"{stream_name},现在状态: {sub_hf.chat_state.chat_status.value},进入专注概率: {sub_hf.interest_chatting.start_hfc_probability}") + # 调试用 from .mai_state_manager import enable_unlimited_hfc_chat - if not enable_unlimited_hfc_chat: if sub_hf.chat_state.chat_status != ChatState.CHAT: continue - # 检查是否满足提升概率 - logger.debug( - f"{log_prefix} 检查子心流: {stream_name},现在概率: {sub_hf.interest_chatting.start_hfc_probability}" - ) + if random.random() >= sub_hf.interest_chatting.start_hfc_probability: continue # 再次检查是否达到上限 if current_focused_count >= focused_limit: - logger.debug(f"{log_prefix} [{stream_name}] 已达专注上限") + logger.debug(f"{stream_name} 已达专注上限") break # 获取最新状态并执行提升 @@ -331,7 +330,7 @@ class SubHeartflowManager: continue logger.info( - f"{log_prefix} [{stream_name}] 触发 认真水群 (概率={current_subflow.interest_chatting.start_hfc_probability:.2f})" + f"{stream_name} 触发 认真水群 (概率={current_subflow.interest_chatting.start_hfc_probability:.2f})" ) # 执行状态提升 @@ -372,12 +371,6 @@ class SubHeartflowManager: stream_name = chat_manager.get_stream_name(flow_id) or flow_id log_prefix = f"[{stream_name}]" - # --- Private chat check (redundant due to filter above, but safe) --- - # if not sub_hf_to_evaluate.is_group_chat: - # logger.debug(f"{log_prefix} 是私聊,跳过 CHAT 状态评估。") - # return - # --- End check --- - # 3. 检查 CHAT 上限 current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) if current_chat_count >= chat_limit: @@ -403,9 +396,9 @@ class SubHeartflowManager: first_observation = sub_hf_to_evaluate.observations[0] # 喵~第一个观察者肯定存在的说 await first_observation.observe() current_chat_log = first_observation.talking_message_str or "当前没啥聊天内容。" - _observation_summary = f"最近聊了这些:\n{current_chat_log}" + _observation_summary = f"在[{stream_name}]这个群中,你最近看群友聊了这些:\n{current_chat_log}" - mai_state_description = f"你当前状态: {current_mai_state.value}。" + _mai_state_description = f"你当前状态: {current_mai_state.value}。" individuality = Individuality.get_instance() personality_prompt = individuality.get_prompt(x_person=2, level=2) prompt_personality = f"你正在扮演名为{individuality.name}的人类,{personality_prompt}" @@ -414,27 +407,26 @@ class SubHeartflowManager: chat_status_lines = [] if chatting_group_names: chat_status_lines.append( - f"正在闲聊 ({current_chat_count}/{chat_limit}): {', '.join(chatting_group_names)}" + f"正在这些群闲聊 ({current_chat_count}/{chat_limit}): {', '.join(chatting_group_names)}" ) if focused_group_names: chat_status_lines.append( - f"正在专注 ({current_focused_count}/{focused_limit}): {', '.join(focused_group_names)}" + f"正在这些群专注的聊天 ({current_focused_count}/{focused_limit}): {', '.join(focused_group_names)}" ) chat_status_prompt = "当前没有在任何群聊中。" # 默认消息喵~ if chat_status_lines: - chat_status_prompt = "当前聊天情况:\n" + "\n".join(chat_status_lines) # 拼接状态信息 + chat_status_prompt = "当前聊天情况,你已经参与了下面这几个群的聊天:\n" + "\n".join(chat_status_lines) # 拼接状态信息 prompt = ( - f"{prompt_personality}\\n" - f"你当前没在 [{stream_name}] 群聊天。\\n" - f"{mai_state_description}\\n" - f"{chat_status_prompt}\\n" # <-- 喵!用了新的状态信息~ - f"{_observation_summary}\\n---\\n" - f"基于以上信息,你想不想开始在这个群闲聊?\\n" - f"请说明理由,并以 JSON 格式回答,包含 'decision' (布尔值) 和 'reason' (字符串)。\\n" - f'例如:{{"decision": true, "reason": "看起来挺热闹的,插个话"}}\\n' - f'例如:{{"decision": false, "reason": "已经聊了好多,休息一下"}}\\n' + f"{prompt_personality}\n" + f"{chat_status_prompt}\n" # <-- 喵!用了新的状态信息~ + f"你当前尚未加入 [{stream_name}] 群聊天。\n" + f"{_observation_summary}\n---\n" + f"基于以上信息,你想不想开始在这个群闲聊?\n" + f"请说明理由,并以 JSON 格式回答,包含 'decision' (布尔值) 和 'reason' (字符串)。\n" + f'例如:{{"decision": true, "reason": "看起来挺热闹的,插个话"}}\n' + f'例如:{{"decision": false, "reason": "已经聊了好多,休息一下"}}\n' f"请只输出有效的 JSON 对象。" ) # --- 结束修改 --- @@ -493,7 +485,6 @@ class SubHeartflowManager: checked_count = len(subflows_snapshot) if not subflows_snapshot: - # logger.debug(f"{log_prefix_task} 没有子心流需要检查超时。") return for sub_hf in subflows_snapshot: @@ -509,25 +500,33 @@ class SubHeartflowManager: reason = "" try: - # 使用变量名 last_bot_dong_zuo_time 替代 last_bot_activity_time last_bot_dong_zuo_time = sub_hf.get_normal_chat_last_speak_time() if last_bot_dong_zuo_time > 0: current_time = time.time() - # 使用变量名 time_since_last_bb 替代 time_since_last_reply time_since_last_bb = current_time - last_bot_dong_zuo_time + minutes_since_last_bb = time_since_last_bb / 60 - if time_since_last_bb > NORMAL_CHAT_TIMEOUT_SECONDS: + # 60分钟强制退出 + if minutes_since_last_bb >= 60: should_deactivate = True - reason = f"超过 {NORMAL_CHAT_TIMEOUT_SECONDS / 60:.0f} 分钟没 BB" - logger.info( - f"{log_prefix} 太久没有发言 ({reason}),不看了。上次活动时间: {last_bot_dong_zuo_time:.0f}" - ) - # else: - # logger.debug(f"{log_prefix} Bot活动时间未超时 ({time_since_last_bb:.0f}s < {NORMAL_CHAT_TIMEOUT_SECONDS}s),保持 CHAT 状态。") - # else: - # 如果没有记录到Bot的活动时间,暂时不因为超时而转换状态 - # logger.debug(f"{log_prefix} 未找到有效的 Bot 最后活动时间记录,不执行超时检查。") + reason = "超过60分钟未发言,强制退出" + else: + # 根据时间区间确定退出概率 + exit_probability = 0 + if minutes_since_last_bb < 5: + exit_probability = 0.01 # 1% + elif minutes_since_last_bb < 15: + exit_probability = 0.02 # 2% + elif minutes_since_last_bb < 30: + exit_probability = 0.04 # 4% + else: + exit_probability = 0.08 # 8% + + # 随机判断是否退出 + if random.random() < exit_probability: + should_deactivate = True + reason = f"已{minutes_since_last_bb:.1f}分钟未发言,触发{exit_probability*100:.0f}%退出概率" except AttributeError: logger.error( @@ -536,7 +535,7 @@ class SubHeartflowManager: except Exception as e: logger.error(f"{log_prefix} 检查 Bot 超时状态时出错: {e}", exc_info=True) - # --- 执行状态转换(如果超时) --- + # 执行状态转换(如果超时) if should_deactivate: logger.debug(f"{log_prefix} 因超时 ({reason}),尝试转换为 ABSENT 状态。") await sub_hf.change_chat_state(ChatState.ABSENT) @@ -816,11 +815,6 @@ class SubHeartflowManager: if has_new: is_active = True logger.debug(f"{log_prefix} 检测到新消息,标记为活跃。") - # 可选:检查兴趣度是否大于0 (如果需要) - # interest_level = await sub_hf.interest_chatting.get_interest() - # if interest_level > 0: - # is_active = True - # logger.debug(f"{log_prefix} 检测到兴趣度 > 0 ({interest_level:.2f}),标记为活跃。") else: logger.warning(f"{log_prefix} 无法获取主要观察者来检查活动状态。") @@ -850,56 +844,3 @@ class SubHeartflowManager: logger.debug( f"{log_prefix_task} 完成,共检查 {checked_count} 个私聊,{transitioned_count} 个转换为 FOCUSED。" ) - - # --- 结束新增 --- # - - # --- 结束新增:处理来自 HeartFChatting 的状态转换请求 --- # - - # 临时函数,用于GUI切换,有api后删除 - # async def detect_command_from_gui(self): - # """检测来自GUI的命令""" - # command_file = Path("temp_command/gui_command.json") - # if not command_file.exists(): - # return - - # try: - # # 读取并解析命令文件 - # command_data = json.loads(command_file.read_text()) - # subflow_id = command_data.get("subflow_id") - # target_state = command_data.get("target_state") - - # if not subflow_id or not target_state: - # logger.warning("GUI命令文件格式不正确,缺少必要字段") - # return - - # # 尝试转换为ChatState枚举 - # try: - # target_state_enum = ChatState[target_state.upper()] - # except KeyError: - # logger.warning(f"无效的目标状态: {target_state}") - # command_file.unlink() - # return - - # # 执行状态转换 - # await self.force_change_by_gui(subflow_id, target_state_enum) - - # # 转换成功后删除文件 - # command_file.unlink() - # logger.debug(f"已处理GUI命令并删除命令文件: {command_file}") - - # except json.JSONDecodeError: - # logger.warning("GUI命令文件不是有效的JSON格式") - # except Exception as e: - # logger.error(f"处理GUI命令时发生错误: {e}", exc_info=True) - - # async def force_change_by_gui(self, subflow_id: Any, target_state: ChatState): - # """强制改变指定子心流的状态""" - # async with self._lock: - # subflow = self.subheartflows.get(subflow_id) - # if not subflow: - # logger.warning(f"[强制状态转换] 尝试转换不存在的子心流 {subflow_id} 到 {target_state.value}") - # return - # await subflow.change_chat_state(target_state) - # logger.info(f"[强制状态转换] 成功将 {subflow_id} 的状态转换为 {target_state.value}") - - # --- 结束新增 --- # diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 1c1372c5..e921f85c 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -26,7 +26,7 @@ logger = get_logger("chat") class NormalChat: - def __init__(self, chat_stream: ChatStream, interest_dict: dict): + def __init__(self, chat_stream: ChatStream, interest_dict: dict = None): """初始化 NormalChat 实例。只进行同步操作。""" # Basic info from chat_stream (sync) From c7533a81605f2e01b0564cec2e26ecb96c38b530 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 9 May 2025 21:01:05 +0800 Subject: [PATCH 14/22] fix: ruff --- src/heart_flow/sub_heartflow.py | 5 ++--- src/heart_flow/sub_mind.py | 18 +++++------------- src/heart_flow/subheartflow_manager.py | 19 +++++++++++-------- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index de26d4f6..e2a36dbd 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -110,7 +110,7 @@ class SubHeartflow: logger.error(f"{self.log_prefix} 停止 NormalChat 监控任务时出错: {e}") logger.error(traceback.format_exc()) - async def _start_normal_chat(self, rewind = False) -> bool: + async def _start_normal_chat(self, rewind=False) -> bool: """ 启动 NormalChat 实例,并进行异步初始化。 进入 CHAT 状态时使用。 @@ -128,7 +128,6 @@ class SubHeartflow: if rewind: self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict()) else: - self.normal_chat_instance = NormalChat(chat_stream=chat_stream) # 进行异步初始化 @@ -252,7 +251,7 @@ class SubHeartflow: elif new_state == ChatState.ABSENT: logger.info(f"{log_prefix} 进入 ABSENT 状态,停止所有聊天活动...") await self.clear_interest_dict() - + await self._stop_normal_chat() await self._stop_heart_fc_chat() state_changed = True # 总是可以成功转换到 ABSENT diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index b240f744..31f57159 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -233,7 +233,7 @@ class SubMind: keywords = [word for word in chat_words if len(word) > 1] # 去重并限制数量 keywords = list(set(keywords))[:5] - + logger.debug(f"{self.log_prefix} 提取的关键词: {keywords}") # 检查已有记忆,过滤掉已存在的主题 existing_topics = set() @@ -243,28 +243,20 @@ class SubMind: # 过滤掉已存在的主题 filtered_keywords = [k for k in keywords if k not in existing_topics] - + if not filtered_keywords: logger.debug(f"{self.log_prefix} 所有关键词对应的记忆都已存在,跳过记忆提取") else: # 调用记忆系统获取相关记忆 related_memory = await HippocampusManager.get_instance().get_memory_from_topic( - valid_keywords=filtered_keywords, - max_memory_num=3, - max_memory_length=2, - max_depth=3 + valid_keywords=filtered_keywords, max_memory_num=3, max_memory_length=2, max_depth=3 ) logger.debug(f"{self.log_prefix} 获取到的记忆: {related_memory}") - + if related_memory: for topic, memory in related_memory: - new_item = { - "type": "memory", - "id": topic, - "content": memory, - "ttl": 3 - } + new_item = {"type": "memory", "id": topic, "content": memory, "ttl": 3} self.structured_info.append(new_item) logger.debug(f"{self.log_prefix} 添加新记忆: {topic} - {memory}") else: diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index ba23c70d..c074d29a 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -280,7 +280,7 @@ class SubHeartflowManager: # --- 新增:检查是否允许进入 FOCUS 模式 --- # if not global_config.allow_focus_mode: if int(time.time()) % 60 == 0: # 每60秒输出一次日志避免刷屏 - logger.trace(f"未开启 FOCUSED 状态 (allow_focus_mode=False)") + logger.trace("未开启 FOCUSED 状态 (allow_focus_mode=False)") return # 如果不允许,直接返回 # --- 结束新增 --- @@ -298,24 +298,25 @@ class SubHeartflowManager: for sub_hf in list(self.subheartflows.values()): flow_id = sub_hf.subheartflow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id - + # 跳过非CHAT状态或已经是FOCUSED状态的子心流 if sub_hf.chat_state.chat_status == ChatState.FOCUSED: continue - - + if sub_hf.interest_chatting.start_hfc_probability == 0: continue else: - logger.debug(f"{stream_name},现在状态: {sub_hf.chat_state.chat_status.value},进入专注概率: {sub_hf.interest_chatting.start_hfc_probability}") + logger.debug( + f"{stream_name},现在状态: {sub_hf.chat_state.chat_status.value},进入专注概率: {sub_hf.interest_chatting.start_hfc_probability}" + ) # 调试用 from .mai_state_manager import enable_unlimited_hfc_chat + if not enable_unlimited_hfc_chat: if sub_hf.chat_state.chat_status != ChatState.CHAT: continue - if random.random() >= sub_hf.interest_chatting.start_hfc_probability: continue @@ -416,7 +417,9 @@ class SubHeartflowManager: chat_status_prompt = "当前没有在任何群聊中。" # 默认消息喵~ if chat_status_lines: - chat_status_prompt = "当前聊天情况,你已经参与了下面这几个群的聊天:\n" + "\n".join(chat_status_lines) # 拼接状态信息 + chat_status_prompt = "当前聊天情况,你已经参与了下面这几个群的聊天:\n" + "\n".join( + chat_status_lines + ) # 拼接状态信息 prompt = ( f"{prompt_personality}\n" @@ -526,7 +529,7 @@ class SubHeartflowManager: # 随机判断是否退出 if random.random() < exit_probability: should_deactivate = True - reason = f"已{minutes_since_last_bb:.1f}分钟未发言,触发{exit_probability*100:.0f}%退出概率" + reason = f"已{minutes_since_last_bb:.1f}分钟未发言,触发{exit_probability * 100:.0f}%退出概率" except AttributeError: logger.error( From 5d31cc8bbe52b121f4bcab0bf75a820969933afb Mon Sep 17 00:00:00 2001 From: infinitycat Date: Fri, 9 May 2025 21:20:31 +0800 Subject: [PATCH 15/22] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0docker=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E7=9A=84tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 54 +++++++++++++++++------------- docker-compose.yml | 4 +-- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 605d838c..36c7604f 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -6,10 +6,9 @@ on: - main - classical - dev - - new_knowledge tags: - - 'v*' - workflow_dispatch: + - "v*.*.*" + - "v*" jobs: build-and-push: @@ -20,6 +19,11 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Clone maim_message run: git clone https://github.com/MaiM-with-u/maim_message maim_message @@ -29,6 +33,8 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + with: + buildkitd-flags: --debug - name: Login to Docker Hub uses: docker/login-action@v3 @@ -36,20 +42,18 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Determine Image Tags - id: tags - run: | - if [[ "${{ github.ref }}" == refs/tags/* ]]; then - echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:${{ github.ref_name }},${{ secrets.DOCKERHUB_USERNAME }}/maimbot:latest" >> $GITHUB_OUTPUT - elif [ "${{ github.ref }}" == "refs/heads/main" ]; then - echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:main,${{ secrets.DOCKERHUB_USERNAME }}/maimbot:main-$(date -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT - elif [ "${{ github.ref }}" == "refs/heads/classical" ]; then - echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:classical,${{ secrets.DOCKERHUB_USERNAME }}/maimbot:classical-$(date -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT - elif [ "${{ github.ref }}" == "refs/heads/dev" ]; then - echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:dev,${{ secrets.DOCKERHUB_USERNAME }}/maimbot:dev-$(date -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT - elif [ "${{ github.ref }}" == "refs/heads/new_knowledge" ]; then - echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:knowledge,${{ secrets.DOCKERHUB_USERNAME }}/maimbot:knowledge-$(date -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT - fi + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot + tags: | + type=ref,event=branch + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha - name: Build and Push Docker Image uses: docker/build-push-action@v5 @@ -57,10 +61,14 @@ jobs: context: . file: ./Dockerfile platforms: linux/amd64,linux/arm64 - tags: ${{ steps.tags.outputs.tags }} + tags: ${{ steps.meta.outputs.tags }} push: true - cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:buildcache - cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:buildcache,mode=max - labels: | - org.opencontainers.image.created=${{ steps.tags.outputs.date_tag }} - org.opencontainers.image.revision=${{ github.sha }} \ No newline at end of file + cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:buildcache + cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:buildcache,mode=max + labels: ${{ steps.meta.outputs.labels }} + provenance: true + sbom: true + build-args: | + BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') + VCS_REF=${{ github.sha }} + outputs: type=image,push=true \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 000d00c3..363fafc2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,8 +16,8 @@ services: - maim_bot core: container_name: maim-bot-core - image: sengokucola/maimbot:main - # image: infinitycat/maimbot:main + image: sengokucola/maibot:main + # image: infinitycat/maibot:main environment: - TZ=Asia/Shanghai # - EULA_AGREE=35362b6ea30f12891d46ef545122e84a # 同意EULA From 3323c8dc498984cb938fc16f531dff05e8c5bc21 Mon Sep 17 00:00:00 2001 From: infinitycat Date: Fri, 9 May 2025 21:42:00 +0800 Subject: [PATCH 16/22] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0docker-compose?= =?UTF-8?q?=E7=9A=84tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 363fafc2..2392f707 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,8 +16,11 @@ services: - maim_bot core: container_name: maim-bot-core - image: sengokucola/maibot:main - # image: infinitycat/maibot:main + image: sengokucola/maibot:latest + # image: infinitycat/maibot:latest + # dev + # image: sengokucola/maibot:dev + # image: infinitycat/maibot:dev environment: - TZ=Asia/Shanghai # - EULA_AGREE=35362b6ea30f12891d46ef545122e84a # 同意EULA From 835efd5daae12ef268bb016d9e12f8e9a184fc9c Mon Sep 17 00:00:00 2001 From: infinitycat Date: Sat, 10 May 2025 01:41:56 +0800 Subject: [PATCH 17/22] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84Docker=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E6=9E=84=E5=BB=BA=E6=B5=81=E7=A8=8B=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=A4=9A=E5=B9=B3=E5=8F=B0=E6=94=AF=E6=8C=81=E5=92=8C?= =?UTF-8?q?=E6=91=98=E8=A6=81=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 129 +++++++++++++++++++++++------ 1 file changed, 102 insertions(+), 27 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 36c7604f..3fce193b 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -10,20 +10,58 @@ on: - "v*.*.*" - "v*" +env: + REGISTRY_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/maibot + jobs: - build-and-push: + prepare: runs-on: ubuntu-latest - env: - DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USERNAME }} - DATE_TAG: $(date -u +'%Y-%m-%dT%H-%M-%S') + outputs: + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + bake-file: ${{ steps.meta.outputs.bake-file }} steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + build: + runs-on: ubuntu-latest + needs: prepare + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm64 + - linux/arm/v7 + - linux/arm/v6 + - linux/386 + - linux/loong64 + steps: + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Clone maim_message run: git clone https://github.com/MaiM-with-u/maim_message maim_message @@ -31,6 +69,9 @@ jobs: - name: Clone lpmm run: git clone https://github.com/MaiM-with-u/MaiMBot-LPMM.git MaiMBot-LPMM + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: @@ -42,33 +83,67 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot - tags: | - type=ref,event=branch - type=ref,event=tag - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha - - - name: Build and Push Docker Image + - name: Build and push by digest + id: build uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - push: true - cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:buildcache - cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:buildcache,mode=max - labels: ${{ steps.meta.outputs.labels }} + platforms: ${{ matrix.platform }} + labels: ${{ needs.prepare.outputs.labels }} + cache-from: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }} + cache-to: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max provenance: true sbom: true build-args: | BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') VCS_REF=${{ github.sha }} - outputs: type=image,push=true \ No newline at end of file + outputs: type=image,push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: + - prepare + - build + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "${{ needs.prepare.outputs.tags }}") \ + $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) + + - name: Inspect image + run: | + tags_json='${{ needs.prepare.outputs.tags }}' + first_tag=$(echo $tags_json | jq -r '.tags[0]') + docker buildx imagetools inspect $first_tag \ No newline at end of file From de1c36f8e8ee64dd0ea2abaca8782ab99b56f211 Mon Sep 17 00:00:00 2001 From: infinitycat Date: Sat, 10 May 2025 01:46:17 +0800 Subject: [PATCH 18/22] =?UTF-8?q?feat:=20=E5=9C=A8Docker=E9=95=9C=E5=83=8F?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E6=B5=81=E7=A8=8B=E4=B8=AD=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E6=94=AF=E6=8C=81=EF=BC=8C=E4=BB=A5=E4=BE=BF?= =?UTF-8?q?=E4=BA=8E=E7=89=88=E6=9C=AC=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 3fce193b..097fdac3 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -91,6 +91,7 @@ jobs: file: ./Dockerfile platforms: ${{ matrix.platform }} labels: ${{ needs.prepare.outputs.labels }} + tags: ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}-${{ env.PLATFORM_PAIR }} cache-from: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }} cache-to: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max provenance: true From 4fc33278c98e9cf0d97b5a238f9ed69198de8bbf Mon Sep 17 00:00:00 2001 From: infinitycat Date: Sat, 10 May 2025 01:51:09 +0800 Subject: [PATCH 19/22] =?UTF-8?q?feat:=20=E7=B2=BE=E7=AE=80Docker=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E6=9E=84=E5=BB=BA=E6=B5=81=E7=A8=8B=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=B9=B6=E6=9B=B4=E6=96=B0=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 097fdac3..fb3d4938 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -48,10 +48,6 @@ jobs: platform: - linux/amd64 - linux/arm64 - - linux/arm/v7 - - linux/arm/v6 - - linux/386 - - linux/loong64 steps: - name: Prepare run: | @@ -91,7 +87,7 @@ jobs: file: ./Dockerfile platforms: ${{ matrix.platform }} labels: ${{ needs.prepare.outputs.labels }} - tags: ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}-${{ env.PLATFORM_PAIR }} + tags: ${{ env.REGISTRY_IMAGE }} cache-from: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }} cache-to: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max provenance: true From f96fffe16eb8981f2c7f657effd1b503cdfadc0c Mon Sep 17 00:00:00 2001 From: infinitycat Date: Sat, 10 May 2025 02:00:59 +0800 Subject: [PATCH 20/22] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0Docker=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E6=9E=84=E5=BB=BA=E6=B5=81=E7=A8=8B=EF=BC=8C=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E4=BD=BF=E7=94=A8=E9=BB=98=E8=AE=A4=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E6=A0=87=E7=AD=BE=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index fb3d4938..7ea9d86e 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -136,8 +136,16 @@ jobs: - name: Create manifest list and push working-directory: ${{ runner.temp }}/digests run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "${{ needs.prepare.outputs.tags }}") \ - $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) + # 确保至少有一个默认标签 + TAGS="-t ${{ env.REGISTRY_IMAGE }}:latest" + + # 如果 meta 输出的标签不为空,则使用它们 + if [ -n "${{ needs.prepare.outputs.tags }}" ]; then + TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "${{ needs.prepare.outputs.tags }}") + fi + + echo "Using tags: ${TAGS}" + docker buildx imagetools create ${TAGS} $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) - name: Inspect image run: | From 5ad1993fee7d127b1af776e5816497169be24e14 Mon Sep 17 00:00:00 2001 From: infinitycat Date: Sat, 10 May 2025 02:11:26 +0800 Subject: [PATCH 21/22] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96Docker=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E6=9E=84=E5=BB=BA=E6=B5=81=E7=A8=8B=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E6=A0=87=E7=AD=BE=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E9=BB=98=E8=AE=A4=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 7ea9d86e..a2e4cfc8 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -149,6 +149,20 @@ jobs: - name: Inspect image run: | - tags_json='${{ needs.prepare.outputs.tags }}' - first_tag=$(echo $tags_json | jq -r '.tags[0]') - docker buildx imagetools inspect $first_tag \ No newline at end of file + # 使用默认标签 + DEFAULT_TAG="${{ env.REGISTRY_IMAGE }}:latest" + + # 尝试从 prepare 输出中获取标签 + if [ -n "${{ needs.prepare.outputs.tags }}" ]; then + TAGS_JSON='${{ needs.prepare.outputs.tags }}' + FIRST_TAG=$(echo $TAGS_JSON | jq -r '.tags[0]') + if [ -n "$FIRST_TAG" ] && [ "$FIRST_TAG" != "null" ]; then + echo "使用从 metadata 获取的标签: $FIRST_TAG" + docker buildx imagetools inspect $FIRST_TAG + exit 0 + fi + fi + + # 如果没有标签或提取失败,使用默认标签 + echo "使用默认标签: $DEFAULT_TAG" + docker buildx imagetools inspect $DEFAULT_TAG \ No newline at end of file From 606b89c99b233426d90d3289418eec0476e5e27f Mon Sep 17 00:00:00 2001 From: infinitycat Date: Sat, 10 May 2025 02:37:46 +0800 Subject: [PATCH 22/22] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84Docker=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E6=9E=84=E5=BB=BA=E6=B5=81=E7=A8=8B=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9EAMD64=E5=92=8CARM64=E6=9E=B6=E6=9E=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81=EF=BC=8C=E5=B9=B6=E4=BC=98=E5=8C=96=E5=A4=9A=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E6=B8=85=E5=8D=95=E5=88=9B=E5=BB=BA=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 217 +++++++++++++++-------------- 1 file changed, 109 insertions(+), 108 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index a2e4cfc8..ba56b0c2 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -10,50 +10,13 @@ on: - "v*.*.*" - "v*" -env: - REGISTRY_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/maibot - jobs: - prepare: + build-amd64: + name: Build AMD64 Image runs-on: ubuntu-latest - outputs: - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - bake-file: ${{ steps.meta.outputs.bake-file }} + env: + DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USERNAME }} steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY_IMAGE }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha - - build: - runs-on: ubuntu-latest - needs: prepare - strategy: - fail-fast: false - matrix: - platform: - - linux/amd64 - - linux/arm64 - steps: - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - name: Checkout code uses: actions/checkout@v4 with: @@ -65,9 +28,6 @@ jobs: - name: Clone lpmm run: git clone https://github.com/MaiM-with-u/MaiMBot-LPMM.git MaiMBot-LPMM - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: @@ -79,50 +39,61 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push by digest - id: build + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot + tags: | + type=ref,event=branch + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - name: Build and Push AMD64 Docker Image uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile - platforms: ${{ matrix.platform }} - labels: ${{ needs.prepare.outputs.labels }} - tags: ${{ env.REGISTRY_IMAGE }} - cache-from: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }} - cache-to: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max + platforms: linux/amd64 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/maibot:amd64-${{ github.sha }} + push: true + cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:amd64-buildcache + cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:amd64-buildcache,mode=max + labels: ${{ steps.meta.outputs.labels }} provenance: true sbom: true build-args: | BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') VCS_REF=${{ github.sha }} - outputs: type=image,push-by-digest=true,name-canonical=true,push=true + outputs: type=image,push=true - - name: Export digest - run: | - mkdir -p ${{ runner.temp }}/digests - digest="${{ steps.build.outputs.digest }}" - touch "${{ runner.temp }}/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-${{ env.PLATFORM_PAIR }} - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 - - merge: + build-arm64: + name: Build ARM64 Image runs-on: ubuntu-latest - needs: - - prepare - - build + env: + DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USERNAME }} steps: - - name: Download digests - uses: actions/download-artifact@v4 + - name: Checkout code + uses: actions/checkout@v4 with: - path: ${{ runner.temp }}/digests - pattern: digests-* - merge-multiple: true + fetch-depth: 0 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Clone maim_message + run: git clone https://github.com/MaiM-with-u/maim_message maim_message + + - name: Clone lpmm + run: git clone https://github.com/MaiM-with-u/MaiMBot-LPMM.git MaiMBot-LPMM + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + buildkitd-flags: --debug - name: Login to Docker Hub uses: docker/login-action@v3 @@ -130,39 +101,69 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot + tags: | + type=ref,event=branch + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha - - name: Create manifest list and push - working-directory: ${{ runner.temp }}/digests - run: | - # 确保至少有一个默认标签 - TAGS="-t ${{ env.REGISTRY_IMAGE }}:latest" - - # 如果 meta 输出的标签不为空,则使用它们 - if [ -n "${{ needs.prepare.outputs.tags }}" ]; then - TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "${{ needs.prepare.outputs.tags }}") - fi - - echo "Using tags: ${TAGS}" - docker buildx imagetools create ${TAGS} $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) + - name: Build and Push ARM64 Docker Image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/arm64 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/maibot:arm64-${{ github.sha }} + push: true + cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:arm64-buildcache + cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:arm64-buildcache,mode=max + labels: ${{ steps.meta.outputs.labels }} + provenance: true + sbom: true + build-args: | + BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') + VCS_REF=${{ github.sha }} + outputs: type=image,push=true - - name: Inspect image + create-manifest: + name: Create Multi-Arch Manifest + runs-on: ubuntu-latest + needs: + - build-amd64 + - build-arm64 + steps: + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot + tags: | + type=ref,event=branch + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - name: Create and Push Manifest run: | - # 使用默认标签 - DEFAULT_TAG="${{ env.REGISTRY_IMAGE }}:latest" - - # 尝试从 prepare 输出中获取标签 - if [ -n "${{ needs.prepare.outputs.tags }}" ]; then - TAGS_JSON='${{ needs.prepare.outputs.tags }}' - FIRST_TAG=$(echo $TAGS_JSON | jq -r '.tags[0]') - if [ -n "$FIRST_TAG" ] && [ "$FIRST_TAG" != "null" ]; then - echo "使用从 metadata 获取的标签: $FIRST_TAG" - docker buildx imagetools inspect $FIRST_TAG - exit 0 - fi - fi - - # 如果没有标签或提取失败,使用默认标签 - echo "使用默认标签: $DEFAULT_TAG" - docker buildx imagetools inspect $DEFAULT_TAG \ No newline at end of file + # 为每个标签创建多架构镜像 + for tag in $(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ' '); do + echo "Creating manifest for $tag" + docker buildx imagetools create -t $tag \ + ${{ secrets.DOCKERHUB_USERNAME }}/maibot:amd64-${{ github.sha }} \ + ${{ secrets.DOCKERHUB_USERNAME }}/maibot:arm64-${{ github.sha }} + done \ No newline at end of file