diff --git a/.dockerignore b/.dockerignore index fac1bf99a..e1f125bd5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,4 +4,6 @@ __pycache__ *.pyd .DS_Store mongodb -napcat \ No newline at end of file +napcat +docs/ +.github/ \ No newline at end of file diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index f9b5e6658..47fdf5b7f 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -12,18 +12,20 @@ on: - "*.*.*" - "*.*.*-*" +# Workflow's jobs jobs: build-amd64: name: Build AMD64 Image runs-on: ubuntu-latest - env: - DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USERNAME }} + outputs: + digest: ${{ steps.build.outputs.digest }} steps: - - name: Checkout code + - name: Check out git repository uses: actions/checkout@v4 with: fetch-depth: 0 + # Clone required dependencies - name: Clone maim_message run: git clone https://github.com/MaiM-with-u/maim_message maim_message @@ -35,106 +37,93 @@ jobs: with: buildkitd-flags: --debug - - name: Login to Docker Hub + # Log in docker hub + - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + # Generate metadata for Docker images - 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=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha - - name: Build and Push AMD64 Docker Image + # Build and push AMD64 image by digest + - name: Build and push AMD64 + id: build uses: docker/build-push-action@v5 with: context: . - file: ./Dockerfile platforms: linux/amd64 - tags: ${{ secrets.DOCKERHUB_USERNAME }}/maibot:amd64-${{ github.sha }} - push: true + labels: ${{ steps.meta.outputs.labels }} + file: ./Dockerfile 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 + outputs: type=image,name=${{ secrets.DOCKERHUB_USERNAME }}/maibot,push-by-digest=true,name-canonical=true,push=true build-args: | BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') VCS_REF=${{ github.sha }} - outputs: type=image,push=true build-arm64: name: Build ARM64 Image runs-on: ubuntu-latest - env: - DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USERNAME }} + outputs: + digest: ${{ steps.build.outputs.digest }} steps: - - name: Checkout code + - name: Check out git repository uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - + # Clone required dependencies - 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 QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: buildkitd-flags: --debug - - name: Login to Docker Hub + # Log in docker hub + - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + # Generate metadata for Docker images - 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=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha - - name: Build and Push ARM64 Docker Image + # Build and push ARM64 image by digest + - name: Build and push ARM64 + id: build uses: docker/build-push-action@v5 with: context: . + platforms: linux/arm64/v8 + labels: ${{ steps.meta.outputs.labels }} 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 + outputs: type=image,name=${{ secrets.DOCKERHUB_USERNAME }}/maibot,push-by-digest=true,name-canonical=true,push=true build-args: | BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') VCS_REF=${{ github.sha }} - outputs: type=image,push=true create-manifest: name: Create Multi-Arch Manifest @@ -143,12 +132,17 @@ jobs: - build-amd64 - build-arm64 steps: - - name: Login to Docker Hub + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Log in docker hub + - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - + + # Generate metadata for Docker images - name: Docker meta id: meta uses: docker/metadata-action@v5 @@ -161,7 +155,7 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - type=sha + type=sha,prefix=${{ github.ref_name }}-,enable=${{ github.ref_type == 'branch' }} - name: Create and Push Manifest run: | @@ -169,6 +163,6 @@ jobs: 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 }} + ${{ secrets.DOCKERHUB_USERNAME }}/maibot@${{ needs.build-amd64.outputs.digest }} \ + ${{ secrets.DOCKERHUB_USERNAME }}/maibot@${{ needs.build-arm64.outputs.digest }} done \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b2ce0a31e..bcc8a57a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,6 @@ services: restart: always networks: - maim_bot - core: container_name: maim-bot-core #### prod #### @@ -40,7 +39,6 @@ services: restart: always networks: - maim_bot - napcat: environment: - NAPCAT_UID=1000 @@ -57,8 +55,8 @@ services: image: mlikiowa/napcat-docker:latest networks: - maim_bot - sqlite-web: + # 注意:coleifer/sqlite-web 镜像不支持arm64 image: coleifer/sqlite-web container_name: sqlite-web restart: always @@ -70,7 +68,19 @@ services: - SQLITE_DATABASE=MaiMBot/MaiBot.db # 你的数据库文件 networks: - maim_bot - + + # chat2db占用相对较高但是功能强大 + # 内存占用约600m,内存充足推荐选此 + # chat2db: + # image: chat2db/chat2db:latest + # container_name: maim-bot-chat2db + # restart: always + # ports: + # - "10824:10824" + # volumes: + # - ./data/MaiMBot:/data/MaiMBot + # networks: + # - maim_bot networks: maim_bot: driver: bridge diff --git a/src/chat/emoji_system/emoji_manager.py b/src/chat/emoji_system/emoji_manager.py index b10d8b0bf..3511d938b 100644 --- a/src/chat/emoji_system/emoji_manager.py +++ b/src/chat/emoji_system/emoji_manager.py @@ -324,8 +324,6 @@ async def clear_temp_emoji() -> None: os.remove(file_path) logger.debug(f"[清理] 删除: {filename}") - logger.info("[清理] 完成") - async def clean_unused_emojis(emoji_dir: str, emoji_objects: List["MaiEmoji"], removed_count: int) -> int: """清理指定目录中未被 emoji_objects 追踪的表情包文件""" @@ -590,7 +588,7 @@ class EmojiManager: """定期检查表情包完整性和数量""" await self.get_all_emoji_from_db() while True: - logger.info("[扫描] 开始检查表情包完整性...") + # logger.info("[扫描] 开始检查表情包完整性...") await self.check_emoji_file_integrity() await clear_temp_emoji() logger.info("[扫描] 开始扫描新表情包...") diff --git a/src/chat/heart_flow/heartflow.py b/src/chat/heart_flow/heartflow.py index f0e01e838..ca6e8be7b 100644 --- a/src/chat/heart_flow/heartflow.py +++ b/src/chat/heart_flow/heartflow.py @@ -30,7 +30,7 @@ class Heartflow: # 注册子心流 self.subheartflows[subheartflow_id] = new_subflow heartflow_name = get_chat_manager().get_stream_name(subheartflow_id) or subheartflow_id - logger.info(f"[{heartflow_name}] 开始接收消息") + logger.debug(f"[{heartflow_name}] 开始接收消息") return new_subflow except Exception as e: diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index 66ddf362e..52a9751ea 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -119,15 +119,13 @@ class HeartFCMessageReceiver: # current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time)) current_talk_frequency = global_config.chat.get_current_talk_frequency(chat.stream_id) - # 如果消息中包含图片标识,则日志展示为图片 + # 如果消息中包含图片标识,则将 [picid:...] 替换为 [图片] + picid_pattern = r"\[picid:([^\]]+)\]" + processed_plain_text = re.sub(picid_pattern, "[图片]", message.processed_plain_text) - picid_match = re.search(r"\[picid:([^\]]+)\]", message.processed_plain_text) - if picid_match: - logger.info(f"[{mes_name}]{userinfo.user_nickname}: [图片] [当前回复频率: {current_talk_frequency}]") - else: - logger.info( - f"[{mes_name}]{userinfo.user_nickname}:{message.processed_plain_text}[当前回复频率: {current_talk_frequency}]" - ) + logger.info(f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}") + + logger.debug(f"[{mes_name}][当前时段回复频率: {current_talk_frequency}]") # 8. 关系处理 if global_config.relationship.enable_relationship: diff --git a/src/chat/memory_system/memory_activator.py b/src/chat/memory_system/memory_activator.py index b9a6248ff..560fe01a6 100644 --- a/src/chat/memory_system/memory_activator.py +++ b/src/chat/memory_system/memory_activator.py @@ -119,7 +119,8 @@ class MemoryActivator: valid_keywords=keywords, max_memory_num=3, max_memory_length=2, max_depth=3 ) - logger.info(f"当前记忆关键词: {self.cached_keywords} 。获取到的记忆: {related_memory}") + logger.debug(f"当前记忆关键词: {self.cached_keywords} ") + logger.debug(f"获取到的记忆: {related_memory}") # 激活时,所有已有记忆的duration+1,达到3则移除 for m in self.running_memory[:]: diff --git a/src/chat/message_receive/bot.py b/src/chat/message_receive/bot.py index 0bc5bec58..0e94991b6 100644 --- a/src/chat/message_receive/bot.py +++ b/src/chat/message_receive/bot.py @@ -166,9 +166,10 @@ class ChatBot: message_data["message_info"]["group_info"]["group_id"] = str( message_data["message_info"]["group_info"]["group_id"] ) - message_data["message_info"]["user_info"]["user_id"] = str( - message_data["message_info"]["user_info"]["user_id"] - ) + if message_data["message_info"].get("user_info") is not None: + message_data["message_info"]["user_info"]["user_id"] = str( + message_data["message_info"]["user_info"]["user_id"] + ) # print(message_data) # logger.debug(str(message_data)) message = MessageRecv(message_data) diff --git a/src/chat/message_receive/message.py b/src/chat/message_receive/message.py index 091570839..7575e0e53 100644 --- a/src/chat/message_receive/message.py +++ b/src/chat/message_receive/message.py @@ -107,7 +107,9 @@ class MessageRecv(Message): self.processed_plain_text = message_dict.get("processed_plain_text", "") self.detailed_plain_text = message_dict.get("detailed_plain_text", "") self.is_emoji = False + self.has_emoji = False self.is_picid = False + self.has_picid = False self.is_mentioned = None self.priority_mode = "interest" self.priority_info = None @@ -134,25 +136,35 @@ class MessageRecv(Message): """ try: if segment.type == "text": + self.is_picid = False + self.is_emoji = False return segment.data elif segment.type == "image": # 如果是base64图片数据 if isinstance(segment.data, str): + self.has_picid = True self.is_picid = True + self.is_emoji = False image_manager = get_image_manager() # print(f"segment.data: {segment.data}") _, processed_text = await image_manager.process_image(segment.data) return processed_text return "[发了一张图片,网卡了加载不出来]" elif segment.type == "emoji": + self.has_emoji = True self.is_emoji = True + self.is_picid = False if isinstance(segment.data, str): return await get_image_manager().get_emoji_description(segment.data) return "[发了一个表情包,网卡了加载不出来]" elif segment.type == "mention_bot": + self.is_picid = False + self.is_emoji = False self.is_mentioned = float(segment.data) return "" elif segment.type == "priority_info": + self.is_picid = False + self.is_emoji = False if isinstance(segment.data, dict): # 处理优先级信息 self.priority_mode = "priority" diff --git a/src/chat/message_receive/storage.py b/src/chat/message_receive/storage.py index 4e4609b0e..ff53e8472 100644 --- a/src/chat/message_receive/storage.py +++ b/src/chat/message_receive/storage.py @@ -136,7 +136,7 @@ class MessageStorage: if matched_message: # 更新找到的消息记录 Messages.update(message_id=qq_message_id).where(Messages.id == matched_message.id).execute() - logger.info(f"更新消息ID成功: {matched_message.message_id} -> {qq_message_id}") + logger.debug(f"更新消息ID成功: {matched_message.message_id} -> {qq_message_id}") else: logger.debug("未找到匹配的消息") diff --git a/src/chat/normal_chat/normal_chat.py b/src/chat/normal_chat/normal_chat.py index 569584eb5..51642a700 100644 --- a/src/chat/normal_chat/normal_chat.py +++ b/src/chat/normal_chat/normal_chat.py @@ -302,50 +302,44 @@ class NormalChat: logger.info(f"[{self.stream_name}] 在处理上下文中检测到停止信号,退出") break - # 并行处理兴趣消息 - async def process_single_message(msg_id, message, interest_value, is_mentioned): - """处理单个兴趣消息""" - try: - # 在处理每个消息前检查停止状态 - if self._disabled: - logger.debug(f"[{self.stream_name}] 处理消息时检测到停用,跳过消息 {msg_id}") - return + semaphore = asyncio.Semaphore(5) - # 处理消息 - self.adjust_reply_frequency() + async def process_and_acquire(msg_id, message, interest_value, is_mentioned): + """处理单个兴趣消息并管理信号量""" + async with semaphore: + try: + # 在处理每个消息前检查停止状态 + if self._disabled: + logger.debug( + f"[{self.stream_name}] 处理消息时检测到停用,跳过消息 {msg_id}" + ) + return - await self.normal_response( - message=message, - is_mentioned=is_mentioned, - interested_rate=interest_value * self.willing_amplifier, - ) - except asyncio.CancelledError: - logger.debug(f"[{self.stream_name}] 处理消息 {msg_id} 时被取消") - raise # 重新抛出取消异常 - except Exception as e: - logger.error(f"[{self.stream_name}] 处理兴趣消息{msg_id}时出错: {e}") - # 不打印完整traceback,避免日志污染 - finally: - # 无论如何都要清理消息 - self.interest_dict.pop(msg_id, None) + # 处理消息 + self.adjust_reply_frequency() - # 创建并行任务列表 - coroutines = [] - for msg_id, (message, interest_value, is_mentioned) in items_to_process: - coroutine = process_single_message(msg_id, message, interest_value, is_mentioned) - coroutines.append(coroutine) + await self.normal_response( + message=message, + is_mentioned=is_mentioned, + interested_rate=interest_value * self.willing_amplifier, + ) + except asyncio.CancelledError: + logger.debug(f"[{self.stream_name}] 处理消息 {msg_id} 时被取消") + raise # 重新抛出取消异常 + except Exception as e: + logger.error(f"[{self.stream_name}] 处理兴趣消息{msg_id}时出错: {e}") + # 不打印完整traceback,避免日志污染 + finally: + # 无论如何都要清理消息 + self.interest_dict.pop(msg_id, None) - # 并行执行所有任务,限制并发数量避免资源过度消耗 - if coroutines: - # 使用信号量控制并发数,最多同时处理5个消息 - semaphore = asyncio.Semaphore(5) + tasks = [ + process_and_acquire(msg_id, message, interest_value, is_mentioned) + for msg_id, (message, interest_value, is_mentioned) in items_to_process + ] - async def limited_process(coroutine, sem): - async with sem: - await coroutine - - limited_tasks = [limited_process(coroutine, semaphore) for coroutine in coroutines] - await asyncio.gather(*limited_tasks, return_exceptions=True) + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) except asyncio.CancelledError: logger.info(f"[{self.stream_name}] 处理上下文时任务被取消") @@ -469,9 +463,6 @@ class NormalChat: ) -> Optional[list]: """生成普通回复""" try: - logger.info( - f"NormalChat思考:{message.processed_plain_text[:30] + '...' if len(message.processed_plain_text) > 30 else message.processed_plain_text}" - ) person_info_manager = get_person_info_manager() person_id = person_info_manager.get_person_id( message.chat_stream.user_info.platform, message.chat_stream.user_info.user_id @@ -491,10 +482,6 @@ class NormalChat: logger.info(f"对 {message.processed_plain_text} 的回复生成失败") return None - content = " ".join([item[1] for item in reply_set if item[0] == "text"]) - if content: - logger.info(f"{global_config.bot.nickname}的备选回复是:{content}") - return reply_set except Exception as e: @@ -532,7 +519,15 @@ class NormalChat: reasoning = plan_result["action_result"]["reasoning"] is_parallel = plan_result["action_result"].get("is_parallel", False) - logger.info(f"[{self.stream_name}] Planner决策: {action_type}, 理由: {reasoning}, 并行执行: {is_parallel}") + if action_type == "no_action": + logger.info(f"[{self.stream_name}] {global_config.bot.nickname} 决定进行回复") + elif is_parallel: + logger.info( + f"[{self.stream_name}] {global_config.bot.nickname} 决定进行回复, 同时执行{action_type}动作" + ) + else: + logger.info(f"[{self.stream_name}] {global_config.bot.nickname} 决定执行{action_type}动作") + self.action_type = action_type # 更新实例属性 self.is_parallel_action = is_parallel # 新增:保存并行执行标志 @@ -590,10 +585,21 @@ class NormalChat: ) response_set, plan_result = results except asyncio.TimeoutError: + gen_timed_out = not gen_task.done() + plan_timed_out = not plan_task.done() + + timeout_details = [] + if gen_timed_out: + timeout_details.append("回复生成(gen)") + if plan_timed_out: + timeout_details.append("动作规划(plan)") + + timeout_source = " 和 ".join(timeout_details) + logger.warning( - f"[{self.stream_name}] 并行执行回复生成和动作规划超时 ({gather_timeout}秒),正在取消相关任务..." + f"[{self.stream_name}] {timeout_source} 任务超时 ({global_config.chat.thinking_timeout}秒),正在取消相关任务..." ) - print(f"111{self.timeout_count}") + # print(f"111{self.timeout_count}") self.timeout_count += 1 if self.timeout_count > 5: logger.warning( @@ -623,18 +629,23 @@ class NormalChat: elif plan_result: logger.debug(f"[{self.stream_name}] 额外动作处理完成: {self.action_type}") + if response_set: + content = " ".join([item[1] for item in response_set if item[0] == "text"]) + if not response_set or ( self.enable_planner and self.action_type not in ["no_action"] and not self.is_parallel_action ): if not response_set: - logger.info(f"[{self.stream_name}] 模型未生成回复内容") + logger.warning(f"[{self.stream_name}] 模型未生成回复内容") elif self.enable_planner and self.action_type not in ["no_action"] and not self.is_parallel_action: - logger.info(f"[{self.stream_name}] 模型选择其他动作(非并行动作)") + logger.info( + f"[{self.stream_name}] {global_config.bot.nickname} 原本想要回复:{content},但选择执行{self.action_type},不发表回复" + ) # 如果模型未生成回复,移除思考消息 await self._cleanup_thinking_message_by_id(thinking_id) return False - # logger.info(f"[{self.stream_name}] 回复内容: {response_set}") + logger.info(f"[{self.stream_name}] {global_config.bot.nickname} 决定的回复内容: {content}") if self._disabled: logger.info(f"[{self.stream_name}] 已停用,忽略 normal_response。") diff --git a/src/chat/planner_actions/action_manager.py b/src/chat/planner_actions/action_manager.py index c7f9bd6c1..3918831ca 100644 --- a/src/chat/planner_actions/action_manager.py +++ b/src/chat/planner_actions/action_manager.py @@ -96,7 +96,7 @@ class ActionManager: f"从插件系统加载Action组件: {action_name} (插件: {getattr(action_info, 'plugin_name', 'unknown')})" ) - logger.info(f"从插件系统加载了 {len(action_components)} 个Action组件") + logger.info(f"加载了 {len(action_components)} 个Action动作") except Exception as e: logger.error(f"从插件系统加载Action组件失败: {e}") diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 135ea6bac..edd5d010d 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -124,6 +124,11 @@ class ActionPlanner: logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}") if reasoning_content: logger.info(f"{self.log_prefix}规划器推理: {reasoning_content}") + else: + logger.debug(f"{self.log_prefix}规划器原始提示词: {prompt}") + logger.debug(f"{self.log_prefix}规划器原始响应: {llm_content}") + if reasoning_content: + logger.debug(f"{self.log_prefix}规划器推理: {reasoning_content}") except Exception as req_e: logger.error(f"{self.log_prefix}LLM 请求执行失败: {req_e}") diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index d9a7feda0..0b3c25f14 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -188,7 +188,7 @@ class DefaultReplyer: } for key, value in reply_data.items(): if not value: - logger.info(f"{self.log_prefix} 回复数据跳过{key},生成回复时将忽略。") + logger.debug(f"{self.log_prefix} 回复数据跳过{key},生成回复时将忽略。") # 3. 构建 Prompt with Timer("构建Prompt", {}): # 内部计时器,可选保留 @@ -218,11 +218,13 @@ class DefaultReplyer: ) if global_config.debug.show_prompt: - logger.info(f"{self.log_prefix}Prompt:\n{prompt}\n") + logger.info(f"{self.log_prefix}\n{prompt}\n") + else: + logger.debug(f"{self.log_prefix}\n{prompt}\n") content, (reasoning_content, model_name) = await express_model.generate_response_async(prompt) - logger.info(f"最终回复: {content}") + logger.debug(f"replyer生成内容: {content}") except Exception as llm_e: # 精简报错信息 @@ -331,7 +333,7 @@ class DefaultReplyer: ) if selected_expressions: - logger.info(f"{self.log_prefix} 使用处理器选中的{len(selected_expressions)}个表达方式") + logger.debug(f"{self.log_prefix} 使用处理器选中的{len(selected_expressions)}个表达方式") for expr in selected_expressions: if isinstance(expr, dict) and "situation" in expr and "style" in expr: expr_type = expr.get("type", "style") diff --git a/src/chat/utils/chat_message_builder.py b/src/chat/utils/chat_message_builder.py index 2359abf30..ab97f395b 100644 --- a/src/chat/utils/chat_message_builder.py +++ b/src/chat/utils/chat_message_builder.py @@ -551,6 +551,9 @@ def build_readable_messages( show_actions: 是否显示动作记录 """ # 创建messages的深拷贝,避免修改原始列表 + if not messages: + return "" + copy_messages = [msg.copy() for msg in messages] if show_actions and copy_messages: diff --git a/src/common/database/database_model.py b/src/common/database/database_model.py index 500852d00..8c2bf423b 100644 --- a/src/common/database/database_model.py +++ b/src/common/database/database_model.py @@ -252,8 +252,7 @@ class PersonInfo(BaseModel): know_times = FloatField(null=True) # 认识时间 (时间戳) know_since = FloatField(null=True) # 首次印象总结时间 last_know = FloatField(null=True) # 最后一次印象总结时间 - familiarity_value = IntegerField(null=True, default=0) # 熟悉度,0-100,从完全陌生到非常熟悉 - liking_value = IntegerField(null=True, default=50) # 好感度,0-100,从非常厌恶到十分喜欢 + attitude = IntegerField(null=True, default=50) # 态度,0-100,从非常厌恶到十分喜欢 class Meta: # database = db # 继承自 BaseModel diff --git a/src/common/logger.py b/src/common/logger.py index 6be06d241..40fd15070 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -321,14 +321,13 @@ MODULE_COLORS = { # 核心模块 "main": "\033[1;97m", # 亮白色+粗体 (主程序) "api": "\033[92m", # 亮绿色 - "emoji": "\033[92m", # 亮绿色 - "chat": "\033[94m", # 亮蓝色 + "emoji": "\033[33m", # 亮绿色 + "chat": "\033[92m", # 亮蓝色 "config": "\033[93m", # 亮黄色 "common": "\033[95m", # 亮紫色 "tools": "\033[96m", # 亮青色 "lpmm": "\033[96m", "plugin_system": "\033[91m", # 亮红色 - "experimental": "\033[97m", # 亮白色 "person_info": "\033[32m", # 绿色 "individuality": "\033[34m", # 蓝色 "manager": "\033[35m", # 紫色 @@ -339,17 +338,13 @@ MODULE_COLORS = { "planner": "\033[36m", "memory": "\033[34m", "hfc": "\033[96m", - "base_action": "\033[96m", - "action_manager": "\033[32m", + "action_manager": "\033[38;5;166m", # 关系系统 "relation": "\033[38;5;201m", # 深粉色 # 聊天相关模块 "normal_chat": "\033[38;5;81m", # 亮蓝绿色 "normal_chat_response": "\033[38;5;123m", # 青绿色 - "normal_chat_action_modifier": "\033[38;5;111m", # 蓝色 - "normal_chat_planner": "\033[38;5;75m", # 浅蓝色 "heartflow": "\033[38;5;213m", # 粉色 - "heartflow_utils": "\033[38;5;219m", # 浅粉色 "sub_heartflow": "\033[38;5;207m", # 粉紫色 "subheartflow_manager": "\033[38;5;201m", # 深粉色 "background_tasks": "\033[38;5;240m", # 灰色 @@ -359,11 +354,9 @@ MODULE_COLORS = { "message_storage": "\033[38;5;33m", # 深蓝色 # 专注聊天模块 "replyer": "\033[38;5;166m", # 橙色 - "expressor": "\033[38;5;172m", # 黄橙色 - "processor": "\033[38;5;184m", # 黄绿色 "base_processor": "\033[38;5;190m", # 绿黄色 "working_memory": "\033[38;5;22m", # 深绿色 - "memory_activator": "\033[38;5;28m", # 绿色 + "memory_activator": "\033[34m", # 绿色 # 插件系统 "plugin_manager": "\033[38;5;208m", # 红色 "base_plugin": "\033[38;5;202m", # 橙红色 @@ -389,11 +382,9 @@ MODULE_COLORS = { "tool_executor": "\033[38;5;64m", # 深绿色 "base_tool": "\033[38;5;70m", # 绿色 # 工具和实用模块 - "prompt": "\033[38;5;99m", # 紫色 "prompt_build": "\033[38;5;105m", # 紫色 "chat_utils": "\033[38;5;111m", # 蓝色 "chat_image": "\033[38;5;117m", # 浅蓝色 - "typo_gen": "\033[38;5;123m", # 青绿色 "maibot_statistic": "\033[38;5;129m", # 紫色 # 特殊功能插件 "mute_plugin": "\033[38;5;240m", # 灰色 @@ -405,16 +396,13 @@ MODULE_COLORS = { # 数据库和消息 "database_model": "\033[38;5;94m", # 橙褐色 "maim_message": "\033[38;5;100m", # 绿褐色 - # 实验性模块 - "pfc": "\033[38;5;252m", # 浅灰色 # 日志系统 "logger": "\033[38;5;8m", # 深灰色 - "demo": "\033[38;5;15m", # 白色 "confirm": "\033[1;93m", # 黄色+粗体 # 模型相关 "model_utils": "\033[38;5;164m", # 紫红色 "relationship_fetcher": "\033[38;5;170m", # 浅紫色 - "relationship_builder": "\033[38;5;117m", # 浅蓝色 + "relationship_builder": "\033[38;5;93m", # 浅蓝色 } RESET_COLOR = "\033[0m" diff --git a/src/person_info/person_info.py b/src/person_info/person_info.py index 86e3b6fcd..7f22fc2d4 100644 --- a/src/person_info/person_info.py +++ b/src/person_info/person_info.py @@ -48,6 +48,7 @@ person_info_default = { "points": None, "forgotten_points": None, "relation_value": None, + "attitude": 50, } diff --git a/src/person_info/relationship_builder.py b/src/person_info/relationship_builder.py index 11d7e5b47..33ed61c73 100644 --- a/src/person_info/relationship_builder.py +++ b/src/person_info/relationship_builder.py @@ -140,7 +140,7 @@ class RelationshipBuilder: segments.append(new_segment) person_name = get_person_info_manager().get_value_sync(person_id, "person_name") or person_id - logger.info( + logger.debug( f"{self.log_prefix} 眼熟用户 {person_name} 在 {time.strftime('%H:%M:%S', time.localtime(potential_start_time))} - {time.strftime('%H:%M:%S', time.localtime(message_time))} 之间有 {new_segment['message_count']} 条消息" ) self._save_cache() @@ -187,7 +187,9 @@ class RelationshipBuilder: segments.append(new_segment) person_info_manager = get_person_info_manager() person_name = person_info_manager.get_value_sync(person_id, "person_name") or person_id - logger.info(f"{self.log_prefix} 重新眼熟用户 {person_name} 创建新消息段(超过10条消息间隔): {new_segment}") + logger.debug( + f"{self.log_prefix} 重新眼熟用户 {person_name} 创建新消息段(超过10条消息间隔): {new_segment}" + ) self._save_cache() @@ -384,7 +386,7 @@ class RelationshipBuilder: total_message_count = self._get_total_message_count(person_id) if total_message_count >= 45: users_to_build_relationship.append(person_id) - logger.info( + logger.debug( f"{self.log_prefix} 用户 {person_id} 满足关系构建条件,总消息数:{total_message_count},消息段数:{len(segments)}" ) elif total_message_count > 0: @@ -422,7 +424,7 @@ class RelationshipBuilder: # 获取该段的消息(包含边界) segment_messages = get_raw_msg_by_timestamp_with_chat_inclusive(self.chat_id, start_time, end_time) - logger.info( + logger.debug( f"消息段 {i + 1}: {start_date} - {time.strftime('%Y-%m-%d %H:%M', time.localtime(end_time))}, 消息数: {len(segment_messages)}" ) @@ -450,7 +452,7 @@ class RelationshipBuilder: # 按时间排序所有消息(包括间隔标识) processed_messages.sort(key=lambda x: x["time"]) - logger.info(f"为 {person_id} 获取到总共 {len(processed_messages)} 条消息(包含间隔标识)用于印象更新") + logger.debug(f"为 {person_id} 获取到总共 {len(processed_messages)} 条消息(包含间隔标识)用于印象更新") relationship_manager = get_relationship_manager() # 调用原有的更新方法 diff --git a/src/person_info/relationship_manager.py b/src/person_info/relationship_manager.py index 2d37bcda8..12891235c 100644 --- a/src/person_info/relationship_manager.py +++ b/src/person_info/relationship_manager.py @@ -1,12 +1,10 @@ from src.common.logger import get_logger -import math from src.person_info.person_info import PersonInfoManager, get_person_info_manager import time import random from src.llm_models.utils_model import LLMRequest from src.config.config import global_config from src.chat.utils.chat_message_builder import build_readable_messages -from src.manager.mood_manager import mood_manager import json from json_repair import repair_json from datetime import datetime @@ -21,67 +19,11 @@ logger = get_logger("relation") class RelationshipManager: def __init__(self): - self.positive_feedback_value = 0 # 正反馈系统 - self.gain_coefficient = [1.0, 1.0, 1.1, 1.2, 1.4, 1.7, 1.9, 2.0] - self._mood_manager = None - self.relationship_llm = LLMRequest( model=global_config.model.relation, request_type="relationship", # 用于动作规划 ) - @property - def mood_manager(self): - if self._mood_manager is None: - self._mood_manager = mood_manager - return self._mood_manager - - def positive_feedback_sys(self, label: str, stance: str): - """正反馈系统,通过正反馈系数增益情绪变化,根据情绪再影响关系变更""" - - positive_list = [ - "开心", - "惊讶", - "害羞", - ] - - negative_list = [ - "愤怒", - "悲伤", - "恐惧", - "厌恶", - ] - - if label in positive_list: - if 7 > self.positive_feedback_value >= 0: - self.positive_feedback_value += 1 - elif self.positive_feedback_value < 0: - self.positive_feedback_value = 0 - elif label in negative_list: - if -7 < self.positive_feedback_value <= 0: - self.positive_feedback_value -= 1 - elif self.positive_feedback_value > 0: - self.positive_feedback_value = 0 - - if abs(self.positive_feedback_value) > 1: - logger.debug(f"触发mood变更增益,当前增益系数:{self.gain_coefficient[abs(self.positive_feedback_value)]}") - - def mood_feedback(self, value): - """情绪反馈""" - mood_manager = self.mood_manager - mood_gain = mood_manager.current_mood.valence**2 * math.copysign(1, value * mood_manager.current_mood.valence) - value += value * mood_gain - logger.debug(f"当前relationship增益系数:{mood_gain:.3f}") - return value - - def feedback_to_mood(self, mood_value): - """对情绪的反馈""" - coefficient = self.gain_coefficient[abs(self.positive_feedback_value)] - if mood_value > 0 and self.positive_feedback_value > 0 or mood_value < 0 and self.positive_feedback_value < 0: - return mood_value * coefficient - else: - return mood_value / coefficient - @staticmethod async def is_known_some_one(platform, user_id): """判断是否认识某人""" @@ -168,19 +110,6 @@ class RelationshipManager: return relation_prompt - async def _update_list_field(self, person_id: str, field_name: str, new_items: list) -> None: - """更新列表类型的字段,将新项目添加到现有列表中 - - Args: - person_id: 用户ID - field_name: 字段名称 - new_items: 新的项目列表 - """ - person_info_manager = get_person_info_manager() - old_items = await person_info_manager.get_value(person_id, field_name) or [] - updated_items = list(set(old_items + [item for item in new_items if isinstance(item, str) and item])) - await person_info_manager.update_one_field(person_id, field_name, updated_items) - async def update_person_impression(self, person_id, timestamp, bot_engaged_messages=None): """更新用户印象 @@ -194,6 +123,7 @@ class RelationshipManager: person_info_manager = get_person_info_manager() person_name = await person_info_manager.get_value(person_id, "person_name") nickname = await person_info_manager.get_value(person_id, "nickname") + know_times = await person_info_manager.get_value(person_id, "know_times") or 0 alias_str = ", ".join(global_config.bot.alias_names) # personality_block =get_individuality().get_personality_prompt(x_person=2, level=2) @@ -240,7 +170,9 @@ class RelationshipManager: name_mapping[replace_person_name] = f"用户{current_user}{user_count if user_count > 1 else ''}" current_user = chr(ord(current_user) + 1) - readable_messages = self.build_focus_readable_messages(messages=user_messages, target_person_id=person_id) + readable_messages = build_readable_messages( + messages=user_messages, replace_bot_name=True, timestamp_mode="normal_no_YMD", truncate=True + ) if not readable_messages: return @@ -385,73 +317,118 @@ class RelationshipManager: # 如果points超过10条,按权重随机选择多余的条目移动到forgotten_points if len(current_points) > 10: - # 获取现有forgotten_points - forgotten_points = await person_info_manager.get_value(person_id, "forgotten_points") or [] - if isinstance(forgotten_points, str): - try: - forgotten_points = json.loads(forgotten_points) - except json.JSONDecodeError: - logger.error(f"解析forgotten_points JSON失败: {forgotten_points}") - forgotten_points = [] - elif not isinstance(forgotten_points, list): + current_points = await self._update_impression(person_id, current_points, timestamp) + + # 更新数据库 + await person_info_manager.update_one_field( + person_id, "points", json.dumps(current_points, ensure_ascii=False, indent=None) + ) + + await person_info_manager.update_one_field(person_id, "know_times", know_times + 1) + know_since = await person_info_manager.get_value(person_id, "know_since") or 0 + if know_since == 0: + await person_info_manager.update_one_field(person_id, "know_since", timestamp) + await person_info_manager.update_one_field(person_id, "last_know", timestamp) + + logger.debug(f"{person_name} 的印象更新完成") + + async def _update_impression(self, person_id, current_points, timestamp): + # 获取现有forgotten_points + person_info_manager = get_person_info_manager() + + person_name = await person_info_manager.get_value(person_id, "person_name") + nickname = await person_info_manager.get_value(person_id, "nickname") + know_times = await person_info_manager.get_value(person_id, "know_times") or 0 + attitude = await person_info_manager.get_value(person_id, "attitude") or 50 + + # 根据熟悉度,调整印象和简短印象的最大长度 + if know_times > 300: + max_impression_length = 2000 + max_short_impression_length = 800 + elif know_times > 100: + max_impression_length = 1000 + max_short_impression_length = 500 + elif know_times > 50: + max_impression_length = 500 + max_short_impression_length = 300 + elif know_times > 10: + max_impression_length = 200 + max_short_impression_length = 100 + else: + max_impression_length = 100 + max_short_impression_length = 50 + + # 根据好感度,调整印象和简短印象的最大长度 + attitude_multiplier = (abs(100 - attitude) / 100) + 1 + max_impression_length = max_impression_length * attitude_multiplier + max_short_impression_length = max_short_impression_length * attitude_multiplier + + forgotten_points = await person_info_manager.get_value(person_id, "forgotten_points") or [] + if isinstance(forgotten_points, str): + try: + forgotten_points = json.loads(forgotten_points) + except json.JSONDecodeError: + logger.error(f"解析forgotten_points JSON失败: {forgotten_points}") forgotten_points = [] + elif not isinstance(forgotten_points, list): + forgotten_points = [] - # 计算当前时间 - current_time = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") + # 计算当前时间 + current_time = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") - # 计算每个点的最终权重(原始权重 * 时间权重) - weighted_points = [] - for point in current_points: - time_weight = self.calculate_time_weight(point[2], current_time) - final_weight = point[1] * time_weight - weighted_points.append((point, final_weight)) + # 计算每个点的最终权重(原始权重 * 时间权重) + weighted_points = [] + for point in current_points: + time_weight = self.calculate_time_weight(point[2], current_time) + final_weight = point[1] * time_weight + weighted_points.append((point, final_weight)) - # 计算总权重 - total_weight = sum(w for _, w in weighted_points) + # 计算总权重 + total_weight = sum(w for _, w in weighted_points) - # 按权重随机选择要保留的点 - remaining_points = [] - points_to_move = [] + # 按权重随机选择要保留的点 + remaining_points = [] + points_to_move = [] - # 对每个点进行随机选择 - for point, weight in weighted_points: - # 计算保留概率(权重越高越可能保留) - keep_probability = weight / total_weight + # 对每个点进行随机选择 + for point, weight in weighted_points: + # 计算保留概率(权重越高越可能保留) + keep_probability = weight / total_weight - if len(remaining_points) < 10: - # 如果还没达到30条,直接保留 - remaining_points.append(point) + if len(remaining_points) < 10: + # 如果还没达到30条,直接保留 + remaining_points.append(point) + else: + # 随机决定是否保留 + if random.random() < keep_probability: + # 保留这个点,随机移除一个已保留的点 + idx_to_remove = random.randrange(len(remaining_points)) + points_to_move.append(remaining_points[idx_to_remove]) + remaining_points[idx_to_remove] = point else: - # 随机决定是否保留 - if random.random() < keep_probability: - # 保留这个点,随机移除一个已保留的点 - idx_to_remove = random.randrange(len(remaining_points)) - points_to_move.append(remaining_points[idx_to_remove]) - remaining_points[idx_to_remove] = point - else: - # 不保留这个点 - points_to_move.append(point) + # 不保留这个点 + points_to_move.append(point) - # 更新points和forgotten_points - current_points = remaining_points - forgotten_points.extend(points_to_move) + # 更新points和forgotten_points + current_points = remaining_points + forgotten_points.extend(points_to_move) - # 检查forgotten_points是否达到5条 - if len(forgotten_points) >= 10: - # 构建压缩总结提示词 - alias_str = ", ".join(global_config.bot.alias_names) + # 检查forgotten_points是否达到10条 + if len(forgotten_points) >= 10: + # 构建压缩总结提示词 + alias_str = ", ".join(global_config.bot.alias_names) - # 按时间排序forgotten_points - forgotten_points.sort(key=lambda x: x[2]) + # 按时间排序forgotten_points + forgotten_points.sort(key=lambda x: x[2]) - # 构建points文本 - points_text = "\n".join( - [f"时间:{point[2]}\n权重:{point[1]}\n内容:{point[0]}" for point in forgotten_points] - ) + # 构建points文本 + points_text = "\n".join( + [f"时间:{point[2]}\n权重:{point[1]}\n内容:{point[0]}" for point in forgotten_points] + ) - impression = await person_info_manager.get_value(person_id, "impression") or "" + impression = await person_info_manager.get_value(person_id, "impression") or "" - compress_prompt = f""" + compress_prompt = f""" 你的名字是{global_config.bot.nickname},{global_config.bot.nickname}的别名是{alias_str}。 请不要混淆你自己和{global_config.bot.nickname}和{person_name}。 @@ -466,17 +443,17 @@ class RelationshipManager: 你记得ta最近做的事: {points_text} -请输出一段平文本,以陈诉自白的语气,输出你对{person_name}的了解,不要输出任何其他内容。 +请输出一段{max_impression_length}字左右的平文本,以陈诉自白的语气,输出你对{person_name}的了解,不要输出任何其他内容。 """ - # 调用LLM生成压缩总结 - compressed_summary, _ = await self.relationship_llm.generate_response_async(prompt=compress_prompt) + # 调用LLM生成压缩总结 + compressed_summary, _ = await self.relationship_llm.generate_response_async(prompt=compress_prompt) - current_time = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") - compressed_summary = f"截至{current_time},你对{person_name}的了解:{compressed_summary}" + current_time = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") + compressed_summary = f"截至{current_time},你对{person_name}的了解:{compressed_summary}" - await person_info_manager.update_one_field(person_id, "impression", compressed_summary) + await person_info_manager.update_one_field(person_id, "impression", compressed_summary) - compress_short_prompt = f""" + compress_short_prompt = f""" 你的名字是{global_config.bot.nickname},{global_config.bot.nickname}的别名是{alias_str}。 请不要混淆你自己和{global_config.bot.nickname}和{person_name}。 @@ -487,107 +464,76 @@ class RelationshipManager: 1.对{person_name}的直观印象 2.{global_config.bot.nickname}与{person_name}的关系 3.{person_name}的关键信息 -请输出一段平文本,以陈诉自白的语气,输出你对{person_name}的概括,不要输出任何其他内容。 +请输出一段{max_short_impression_length}字左右的平文本,以陈诉自白的语气,输出你对{person_name}的概括,不要输出任何其他内容。 """ - compressed_short_summary, _ = await self.relationship_llm.generate_response_async( - prompt=compress_short_prompt - ) + compressed_short_summary, _ = await self.relationship_llm.generate_response_async( + prompt=compress_short_prompt + ) - # current_time = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") - # compressed_short_summary = f"截至{current_time},你对{person_name}的了解:{compressed_short_summary}" + # current_time = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") + # compressed_short_summary = f"截至{current_time},你对{person_name}的了解:{compressed_short_summary}" - await person_info_manager.update_one_field(person_id, "short_impression", compressed_short_summary) + await person_info_manager.update_one_field(person_id, "short_impression", compressed_short_summary) - relation_value_prompt = f""" + relation_value_prompt = f""" 你的名字是{global_config.bot.nickname}。 你最近对{person_name}的了解如下: {points_text} -请根据以上信息,评估你和{person_name}的关系,给出两个维度的值:熟悉度和好感度。 -1. 了解度 (familiarity_value): 0-100的整数,表示这些信息让你对ta的了解增进程度。 - - 0: 没有任何进一步了解 - - 25: 有点进一步了解 - - 50: 有进一步了解 - - 75: 有更多了解 - - 100: 有了更多重要的了解 +请根据以上信息,评估你和{person_name}的关系,给出你对ta的态度。 -2. **好感度 (liking_value)**: 0-100的整数,表示这些信息让你对ta的喜。 - - 0: 非常厌恶 - - 25: 有点反感 - - 50: 中立/无感(或者文本中无法明显看出) - - 75: 喜欢这个人 - - 100: 非常喜欢/开心对这个人 +态度: 0-100的整数,表示这些信息让你对ta的态度。 +- 0: 非常厌恶 +- 25: 有点反感 +- 50: 中立/无感(或者文本中无法明显看出) +- 75: 喜欢这个人 +- 100: 非常喜欢/开心对这个人 请严格按照json格式输出,不要有其他多余内容: {{ - "familiarity_value": <0-100之间的整数>, - "liking_value": <0-100之间的整数> +"attitude": <0-100之间的整数>, }} """ - try: - relation_value_response, _ = await self.relationship_llm.generate_response_async( - prompt=relation_value_prompt - ) - relation_value_json = json.loads(repair_json(relation_value_response)) - - # 从LLM获取新生成的值 - new_familiarity_value = int(relation_value_json.get("familiarity_value", 0)) - new_liking_value = int(relation_value_json.get("liking_value", 50)) - - # 获取当前的关系值 - old_familiarity_value = await person_info_manager.get_value(person_id, "familiarity_value") or 0 - liking_value = await person_info_manager.get_value(person_id, "liking_value") or 50 - - # 更新熟悉度 - if new_familiarity_value > 25: - familiarity_value = old_familiarity_value + (new_familiarity_value - 25) / 75 - else: - familiarity_value = old_familiarity_value - - # 更新好感度 - if new_liking_value > 50: - liking_value += (new_liking_value - 50) / 50 - elif new_liking_value < 50: - liking_value -= (50 - new_liking_value) / 50 * 1.5 - - await person_info_manager.update_one_field(person_id, "familiarity_value", familiarity_value) - await person_info_manager.update_one_field(person_id, "liking_value", liking_value) - logger.info(f"更新了与 {person_name} 的关系值: 熟悉度={familiarity_value}, 好感度={liking_value}") - except (json.JSONDecodeError, ValueError, TypeError) as e: - logger.error(f"解析relation_value JSON失败或值无效: {e}, 响应: {relation_value_response}") - - forgotten_points = [] - info_list = [] - await person_info_manager.update_one_field( - person_id, "info_list", json.dumps(info_list, ensure_ascii=False, indent=None) + try: + relation_value_response, _ = await self.relationship_llm.generate_response_async( + prompt=relation_value_prompt ) + relation_value_json = json.loads(repair_json(relation_value_response)) + # 从LLM获取新生成的值 + new_attitude = int(relation_value_json.get("attitude", 50)) + + # 获取当前的关系值 + old_attitude = await person_info_manager.get_value(person_id, "attitude") or 50 + + # 更新熟悉度 + if new_attitude > 25: + attitude = old_attitude + (new_attitude - 25) / 75 + else: + attitude = old_attitude + + # 更新好感度 + if new_attitude > 50: + attitude += (new_attitude - 50) / 50 + elif new_attitude < 50: + attitude -= (50 - new_attitude) / 50 * 1.5 + + await person_info_manager.update_one_field(person_id, "attitude", attitude) + logger.info(f"更新了与 {person_name} 的态度: {attitude}") + except (json.JSONDecodeError, ValueError, TypeError) as e: + logger.error(f"解析relation_value JSON失败或值无效: {e}, 响应: {relation_value_response}") + + forgotten_points = [] + info_list = [] await person_info_manager.update_one_field( - person_id, "forgotten_points", json.dumps(forgotten_points, ensure_ascii=False, indent=None) + person_id, "info_list", json.dumps(info_list, ensure_ascii=False, indent=None) ) - # 更新数据库 await person_info_manager.update_one_field( - person_id, "points", json.dumps(current_points, ensure_ascii=False, indent=None) + person_id, "forgotten_points", json.dumps(forgotten_points, ensure_ascii=False, indent=None) ) - know_times = await person_info_manager.get_value(person_id, "know_times") or 0 - await person_info_manager.update_one_field(person_id, "know_times", know_times + 1) - know_since = await person_info_manager.get_value(person_id, "know_since") or 0 - if know_since == 0: - await person_info_manager.update_one_field(person_id, "know_since", timestamp) - await person_info_manager.update_one_field(person_id, "last_know", timestamp) - logger.info(f"{person_name} 的印象更新完成") - - def build_focus_readable_messages(self, messages: list, target_person_id: str = None) -> str: - """格式化消息,处理所有消息内容""" - if not messages: - return "" - - # 直接处理所有消息,不进行过滤 - return build_readable_messages( - messages=messages, replace_bot_name=True, timestamp_mode="normal_no_YMD", truncate=True - ) + return current_points def calculate_time_weight(self, point_time: str, current_time: str) -> float: """计算基于时间的权重系数""" diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index 128214427..cb429dd4c 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -11,7 +11,7 @@ from src.plugin_system.apis import emoji_api from src.plugins.built_in.core_actions.no_reply import NoReplyAction -logger = get_logger("core_actions") +logger = get_logger("emoji") class EmojiAction(BaseAction): @@ -65,7 +65,7 @@ class EmojiAction(BaseAction): return False, f"未找到匹配 '{description}' 的表情包" emoji_base64, emoji_description, matched_emotion = emoji_result - logger.info(f"{self.log_prefix} 找到表情包: {emoji_description}, 匹配情感: {matched_emotion}") + logger.info(f"{self.log_prefix} 找到表达{matched_emotion}的表情包") # 使用BaseAction的便捷方法发送表情包 success = await self.send_emoji(emoji_base64) diff --git a/src/plugins/built_in/core_actions/no_reply.py b/src/plugins/built_in/core_actions/no_reply.py index d06c958a9..a573a39fd 100644 --- a/src/plugins/built_in/core_actions/no_reply.py +++ b/src/plugins/built_in/core_actions/no_reply.py @@ -103,6 +103,7 @@ class NoReplyAction(BaseAction): logger.info(f"{self.log_prefix} 选择不回复(第{count}次),开始摸鱼,原因: {reason}") + # 进入等待状态 while True: current_time = time.time() elapsed_time = current_time - start_time @@ -110,7 +111,9 @@ class NoReplyAction(BaseAction): if global_config.chat.chat_mode == "auto" and self.is_group: # 检查是否超时 if elapsed_time >= self._max_timeout or self._check_no_activity_and_exit_focus(current_time): - logger.info(f"{self.log_prefix} 等待时间过久({self._max_timeout}秒)或过去10分钟完全没有发言,退出专注模式") + logger.info( + f"{self.log_prefix} 等待时间过久({self._max_timeout}秒)或过去10分钟完全没有发言,退出专注模式" + ) # 标记退出专注模式 self.action_data["_system_command"] = "stop_focus_chat" exit_reason = f"{global_config.bot.nickname}(你)等待了{self._max_timeout}秒,或完全没有说话,感觉群里没有新内容,决定退出专注模式,稍作休息" @@ -139,19 +142,9 @@ class NoReplyAction(BaseAction): # 判定条件:累计3条消息或等待超过5秒且有新消息 time_since_last_judge = current_time - last_judge_time - should_judge = ( - new_message_count >= 3 # 累计3条消息 - or (new_message_count > 0 and time_since_last_judge >= 15.0) # 等待超过5秒且有新消息 - ) + should_judge, trigger_reason = self._should_trigger_judge(new_message_count, time_since_last_judge) if should_judge and time_since_last_judge >= min_judge_interval: - # 判断触发原因 - trigger_reason = "" - if new_message_count >= 3: - trigger_reason = f"累计{new_message_count}条消息" - elif time_since_last_judge >= 10.0: - trigger_reason = f"等待{time_since_last_judge:.1f}秒且有新消息" - logger.info(f"{self.log_prefix} 触发判定({trigger_reason}),进行智能判断...") # 获取最近的消息内容用于判断 @@ -164,7 +157,10 @@ class NoReplyAction(BaseAction): if recent_messages: # 使用message_api构建可读的消息字符串 messages_text = message_api.build_readable_messages( - messages=recent_messages, timestamp_mode="normal_no_YMD", truncate=False, show_actions=False + messages=recent_messages, + timestamp_mode="normal_no_YMD", + truncate=False, + show_actions=False, ) # 获取身份信息 @@ -187,81 +183,13 @@ class NoReplyAction(BaseAction): history_block += "\n" # 检查过去10分钟的发言频率 - frequency_block = "" - should_skip_llm_judge = False # 是否跳过LLM判断 - - try: - # 获取过去10分钟的所有消息 - past_10min_time = current_time - 600 # 10分钟前 - all_messages_10min = message_api.get_messages_by_time_in_chat( - chat_id=self.chat_id, - start_time=past_10min_time, - end_time=current_time, - ) - - # 手动过滤bot自己的消息 - bot_message_count = 0 - if all_messages_10min: - user_id = global_config.bot.qq_account - - for message in all_messages_10min: - # 检查消息发送者是否是bot - sender_id = message.get("user_id", "") - - if sender_id == user_id: - bot_message_count += 1 - - talk_frequency_threshold = global_config.chat.get_current_talk_frequency(self.chat_id) * 10 - - if bot_message_count > talk_frequency_threshold: - over_count = bot_message_count - talk_frequency_threshold - - # 根据超过的数量设置不同的提示词和跳过概率 - skip_probability = 0 - if over_count <= 3: - frequency_block = "你感觉稍微有些累,回复的有点多了。\n" - elif over_count <= 5: - frequency_block = "你今天说话比较多,感觉有点疲惫,想要稍微休息一下。\n" - elif over_count <= 8: - frequency_block = "你发现自己说话太多了,感觉很累,想要安静一会儿,除非有重要的事情否则不想回复。\n" - skip_probability = self._skip_probability - else: - frequency_block = "你感觉非常累,想要安静一会儿。\n" - skip_probability = 1 - - # 根据配置和概率决定是否跳过LLM判断 - if self._skip_judge_when_tired and random.random() < skip_probability: - should_skip_llm_judge = True - logger.info( - f"{self.log_prefix} 发言过多(超过{over_count}条),随机决定跳过此次LLM判断(概率{skip_probability * 100:.0f}%)" - ) - - logger.info( - f"{self.log_prefix} 过去10分钟发言{bot_message_count}条,超过阈值{talk_frequency_threshold},添加疲惫提示" - ) - else: - # 回复次数少时的正向提示 - under_count = talk_frequency_threshold - bot_message_count - - if under_count >= talk_frequency_threshold * 0.8: # 回复很少(少于20%) - frequency_block = "你感觉精力充沛,状态很好,积极参与聊天。\n" - elif under_count >= talk_frequency_threshold * 0.5: # 回复较少(少于50%) - frequency_block = "你感觉状态不错。\n" - else: # 刚好达到阈值 - frequency_block = "" - - logger.info( - f"{self.log_prefix} 过去10分钟发言{bot_message_count}条,未超过阈值{talk_frequency_threshold},添加正向提示" - ) - - except Exception as e: - logger.warning(f"{self.log_prefix} 检查发言频率时出错: {e}") - frequency_block = "" + frequency_block, should_skip_llm_judge = self._get_fatigue_status(current_time) # 如果决定跳过LLM判断,直接更新时间并继续等待 - if should_skip_llm_judge: + logger.info(f"{self.log_prefix} 疲劳,继续等待。") last_judge_time = time.time() # 更新判断时间,避免立即重新判断 + start_time = current_time # 更新消息检查的起始时间,以避免重复判断 continue # 跳过本次LLM判断,继续循环等待 # 构建判断上下文 @@ -352,17 +280,14 @@ class NoReplyAction(BaseAction): logger.error(f"{self.log_prefix} 模型判断异常: {e},继续等待") last_judge_time = time.time() # 异常时也更新时间,避免频繁重试 - - # 每10秒输出一次等待状态 - logger.info(f"{self.log_prefix} 开始等待新消息...") if elapsed_time < 60: if int(elapsed_time) % 10 == 0 and int(elapsed_time) > 0: logger.debug(f"{self.log_prefix} 已等待{elapsed_time:.0f}秒,等待新消息...") await asyncio.sleep(1) else: - if int(elapsed_time) % 60 == 0 and int(elapsed_time) > 0: - logger.debug(f"{self.log_prefix} 已等待{elapsed_time / 60:.0f}分钟,等待新消息...") + if int(elapsed_time) % 180 == 0 and int(elapsed_time) > 0: + logger.info(f"{self.log_prefix} 已等待{elapsed_time / 60:.0f}分钟,等待新消息...") await asyncio.sleep(1) # 短暂等待后继续检查 @@ -380,6 +305,109 @@ class NoReplyAction(BaseAction): ) return False, f"不回复动作执行失败: {e}" + def _should_trigger_judge(self, new_message_count: int, time_since_last_judge: float) -> Tuple[bool, str]: + """判断是否应该触发智能判断,并返回触发原因。 + + Args: + new_message_count: 新消息的数量。 + time_since_last_judge: 距离上次判断的时间。 + + Returns: + 一个元组 (should_judge, reason)。 + - should_judge: 一个布尔值,指示是否应该触发判断。 + - reason: 触发判断的原因字符串。 + """ + # 判定条件:累计3条消息或等待超过15秒且有新消息 + should_judge_flag = new_message_count >= 3 or (new_message_count > 0 and time_since_last_judge >= 15.0) + + if not should_judge_flag: + return False, "" + + # 判断触发原因 + if new_message_count >= 3: + return True, f"累计{new_message_count}条消息" + elif new_message_count > 0 and time_since_last_judge >= 15.0: + return True, f"等待{time_since_last_judge:.1f}秒且有新消息" + + return False, "" + + def _get_fatigue_status(self, current_time: float) -> Tuple[str, bool]: + """ + 根据最近的发言频率生成疲劳提示,并决定是否跳过判断。 + + Args: + current_time: 当前时间戳。 + + Returns: + 一个元组 (frequency_block, should_skip_judge)。 + - frequency_block: 疲劳度相关的提示字符串。 + - should_skip_judge: 是否应该跳过LLM判断的布尔值。 + """ + try: + # 获取过去10分钟的所有消息 + past_10min_time = current_time - 600 # 10分钟前 + all_messages_10min = message_api.get_messages_by_time_in_chat( + chat_id=self.chat_id, + start_time=past_10min_time, + end_time=current_time, + ) + + # 手动过滤bot自己的消息 + bot_message_count = 0 + if all_messages_10min: + user_id = global_config.bot.qq_account + for message in all_messages_10min: + sender_id = message.get("user_id", "") + if sender_id == user_id: + bot_message_count += 1 + + talk_frequency_threshold = global_config.chat.get_current_talk_frequency(self.chat_id) * 10 + + if bot_message_count > talk_frequency_threshold: + over_count = bot_message_count - talk_frequency_threshold + skip_probability = 0 + frequency_block = "" + + if over_count <= 3: + frequency_block = "你感觉稍微有些累,回复的有点多了。\n" + elif over_count <= 5: + frequency_block = "你今天说话比较多,感觉有点疲惫,想要稍微休息一下。\n" + elif over_count <= 8: + frequency_block = "你发现自己说话太多了,感觉很累,想要安静一会儿,除非有重要的事情否则不想回复。\n" + skip_probability = self._skip_probability + else: + frequency_block = "你感觉非常累,想要安静一会儿。\n" + skip_probability = 1 + + should_skip_judge = self._skip_judge_when_tired and random.random() < skip_probability + + if should_skip_judge: + logger.info( + f"{self.log_prefix} 发言过多(超过{over_count}条),随机决定跳过此次LLM判断(概率{skip_probability * 100:.0f}%)" + ) + + logger.info( + f"{self.log_prefix} 过去10分钟发言{bot_message_count}条,超过阈值{talk_frequency_threshold},添加疲惫提示" + ) + return frequency_block, should_skip_judge + else: + # 回复次数少时的正向提示 + under_count = talk_frequency_threshold - bot_message_count + frequency_block = "" + if under_count >= talk_frequency_threshold * 0.8: + frequency_block = "你感觉精力充沛,状态很好,积极参与聊天。\n" + elif under_count >= talk_frequency_threshold * 0.5: + frequency_block = "你感觉状态不错。\n" + + logger.info( + f"{self.log_prefix} 过去10分钟发言{bot_message_count}条,未超过阈值{talk_frequency_threshold},添加正向提示" + ) + return frequency_block, False + + except Exception as e: + logger.warning(f"{self.log_prefix} 检查发言频率时出错: {e}") + return "", False + def _check_no_activity_and_exit_focus(self, current_time: float) -> bool: """检查过去10分钟是否完全没有发言,决定是否退出专注模式