From 1e037e5ce9a6dcf515af974f7896572254044c0a Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Tue, 26 Aug 2025 20:20:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(maizone):=20=E6=96=B0=E5=A2=9EQQ=E7=A9=BA?= =?UTF-8?q?=E9=97=B4=E4=BA=92=E9=80=9A=E7=BB=84=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E8=81=8A=E5=A4=A9=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E7=94=9F=E6=88=90=E8=AF=B4=E8=AF=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入了全新的“QQ空间互通组”功能。用户可以配置多个群聊为一个互通组。 在通过指令发布说说时,系统会自动获取这些群聊的近期聊天记录作为上下文,从而生成与当前讨论话题更相关的说说内容。 - 在 `config.toml` 中新增了 `[maizone_intercom]` 配置项用于定义互通组。 - 重构并增强了动态(说说)的拉取逻辑,提高了对不同数据格式的兼容性和解析的稳定性。 - 对项目中的多个文件进行了代码清理,移除了未使用的导入,使代码更加整洁。 --- __main__.py | 1 - plugins/permission_example/plugin.py | 2 +- src/chat/memory_system/instant_memory.py | 1 - src/chat/utils/utils_video.py | 2 - src/config/config.py | 4 +- src/config/official_configs.py | 6 ++ src/manager/monthly_plan_manager.py | 2 - src/manager/schedule_manager.py | 4 +- src/plugin_system/apis/__init__.py | 2 + src/plugin_system/core/component_registry.py | 2 - .../services/content_service.py | 35 +++--- .../services/qzone_service.py | 100 ++++++++++++++---- template/bot_config_template.toml | 15 ++- 13 files changed, 121 insertions(+), 55 deletions(-) diff --git a/__main__.py b/__main__.py index 4ce570197..996e66cf9 100644 --- a/__main__.py +++ b/__main__.py @@ -4,7 +4,6 @@ if __name__ == "__main__": # 设置Python路径并执行bot.py import sys - import os from pathlib import Path # 添加当前目录到Python路径 diff --git a/plugins/permission_example/plugin.py b/plugins/permission_example/plugin.py index 9fcb1a360..8a2909414 100644 --- a/plugins/permission_example/plugin.py +++ b/plugins/permission_example/plugin.py @@ -11,7 +11,7 @@ from src.plugin_system.base.base_plugin import BasePlugin from src.plugin_system.base.base_command import BaseCommand from src.plugin_system.apis.logging_api import get_logger from src.plugin_system.base.config_types import ConfigField -from src.plugin_system.utils.permission_decorators import require_permission, require_master, PermissionChecker +from src.plugin_system.utils.permission_decorators import require_permission, require_master from src.common.message import ChatStream, Message diff --git a/src/chat/memory_system/instant_memory.py b/src/chat/memory_system/instant_memory.py index 025962d00..7865a57e0 100644 --- a/src/chat/memory_system/instant_memory.py +++ b/src/chat/memory_system/instant_memory.py @@ -2,7 +2,6 @@ import time import re import orjson -import ast import traceback from json_repair import repair_json diff --git a/src/chat/utils/utils_video.py b/src/chat/utils/utils_video.py index c966fa626..5e9f118da 100644 --- a/src/chat/utils/utils_video.py +++ b/src/chat/utils/utils_video.py @@ -18,8 +18,6 @@ from pathlib import Path from typing import List, Tuple, Optional, Dict import io from concurrent.futures import ThreadPoolExecutor -from functools import partial -import numpy as np from src.llm_models.utils_model import LLMRequest from src.config.config import global_config, model_config diff --git a/src/config/config.py b/src/config/config.py index 3ea9eaa08..39d4cae85 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -47,7 +47,8 @@ from src.config.official_configs import ( WakeUpSystemConfig, MonthlyPlanSystemConfig, CrossContextConfig, - PermissionConfig + PermissionConfig, + MaizoneIntercomConfig, ) from .api_ada_configs import ( @@ -396,6 +397,7 @@ class Config(ValidatedConfigBase): wakeup_system: WakeUpSystemConfig = Field(default_factory=lambda: WakeUpSystemConfig(), description="唤醒度系统配置") monthly_plan_system: MonthlyPlanSystemConfig = Field(default_factory=lambda: MonthlyPlanSystemConfig(), description="月层计划系统配置") cross_context: CrossContextConfig = Field(default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置") + maizone_intercom: MaizoneIntercomConfig = Field(default_factory=lambda: MaizoneIntercomConfig(), description="Maizone互通组配置") class APIAdapterConfig(ValidatedConfigBase): diff --git a/src/config/official_configs.py b/src/config/official_configs.py index ecbb6e770..c9e3af9bb 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -701,6 +701,12 @@ class CrossContextConfig(ValidatedConfigBase): groups: List[ContextGroup] = Field(default_factory=list, description="上下文共享组列表") +class MaizoneIntercomConfig(ValidatedConfigBase): + """Maizone互通组配置""" + enable: bool = Field(default=False, description="是否启用Maizone互通组功能") + groups: List[ContextGroup] = Field(default_factory=list, description="Maizone互通组列表") + + class PermissionConfig(ValidatedConfigBase): """权限系统配置类""" diff --git a/src/manager/monthly_plan_manager.py b/src/manager/monthly_plan_manager.py index 9ed0d55f1..f5f20cef2 100644 --- a/src/manager/monthly_plan_manager.py +++ b/src/manager/monthly_plan_manager.py @@ -3,8 +3,6 @@ import asyncio from datetime import datetime, timedelta from typing import List, Optional -import orjson -from json_repair import repair_json from src.common.database.monthly_plan_db import ( add_new_plans, diff --git a/src/manager/schedule_manager.py b/src/manager/schedule_manager.py index 2d3d17577..95cfbb0ee 100644 --- a/src/manager/schedule_manager.py +++ b/src/manager/schedule_manager.py @@ -1,6 +1,5 @@ import orjson import asyncio -import random from datetime import datetime, time, timedelta from typing import Optional, List, Dict, Any from lunar_python import Lunar @@ -9,8 +8,7 @@ from pydantic import BaseModel, ValidationError, validator from src.common.database.sqlalchemy_models import Schedule, get_db_session from src.common.database.monthly_plan_db import ( get_smart_plans_for_daily_schedule, - update_plan_usage, - soft_delete_plans # 保留兼容性 + update_plan_usage # 保留兼容性 ) from src.config.config import global_config, model_config from src.llm_models.utils_model import LLMRequest diff --git a/src/plugin_system/apis/__init__.py b/src/plugin_system/apis/__init__.py index f00f18f30..30ff428d7 100644 --- a/src/plugin_system/apis/__init__.py +++ b/src/plugin_system/apis/__init__.py @@ -20,6 +20,7 @@ from src.plugin_system.apis import ( tool_api, permission_api, ) +from src.plugin_system.apis.chat_api import ChatManager as context_api from .logging_api import get_logger from .plugin_register_api import register_plugin @@ -40,4 +41,5 @@ __all__ = [ "register_plugin", "tool_api", "permission_api", + "context_api", ] diff --git a/src/plugin_system/core/component_registry.py b/src/plugin_system/core/component_registry.py index a8117f0c8..69a2d2a3b 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -10,14 +10,12 @@ from src.plugin_system.base.component_types import ( CommandInfo, EventHandlerInfo, PluginInfo, - EventInfo, ComponentType, ) from src.plugin_system.base.base_command import BaseCommand from src.plugin_system.base.base_action import BaseAction from src.plugin_system.base.base_tool import BaseTool from src.plugin_system.base.base_events_handler import BaseEventHandler -from src.plugin_system.base.base_event import BaseEvent logger = get_logger("component_registry") diff --git a/src/plugins/built_in/maizone_refactored/services/content_service.py b/src/plugins/built_in/maizone_refactored/services/content_service.py index edaa7f2ed..6b7900cc3 100644 --- a/src/plugins/built_in/maizone_refactored/services/content_service.py +++ b/src/plugins/built_in/maizone_refactored/services/content_service.py @@ -3,7 +3,7 @@ 内容服务模块 负责生成所有与QQ空间相关的文本内容,例如说说、评论等。 """ -from typing import Callable +from typing import Callable, Optional import datetime from src.common.logger import get_logger @@ -28,11 +28,12 @@ class ContentService: """ self.get_config = get_config - async def generate_story(self, topic: str) -> str: + async def generate_story(self, topic: str, context: Optional[str] = None) -> str: """ - 根据指定主题生成一条QQ空间说说。 + 根据指定主题和可选的上下文生成一条QQ空间说说。 :param topic: 说说的主题。 + :param context: 可选的聊天上下文。 :return: 生成的说说内容,如果失败则返回空字符串。 """ try: @@ -57,22 +58,18 @@ class ContentService: weekday = weekday_names[now.weekday()] # 构建提示词 - if topic: - prompt = f""" - 你是'{bot_personality}',现在是{current_time}({weekday}),你想写一条主题是'{topic}'的说说发表在qq空间上, - {bot_expression} - 不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,可以适当使用颜文字, - 你可以在说说中自然地提及当前的时间(如"今天"、"现在"、"此刻"等),让说说更贴近发布时间, - 只输出一条说说正文的内容,不要有其他的任何正文以外的冗余输出 - """ - else: - prompt = f""" - 你是'{bot_personality}',现在是{current_time}({weekday}),你想写一条说说发表在qq空间上,主题不限 - {bot_expression} - 不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,可以适当使用颜文字, - 你可以在说说中自然地提及当前的时间(如"今天"、"现在"、"此刻"等),让说说更贴近发布时间, - 只输出一条说说正文的内容,不要有其他的任何正文以外的冗余输出 - """ + prompt_topic = f"主题是'{topic}'" if topic else "主题不限" + prompt = f""" + 你是'{bot_personality}',现在是{current_time}({weekday}),你想写一条{prompt_topic}的说说发表在qq空间上。 + {bot_expression} + 不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,可以适当使用颜文字。 + 你可以在说说中自然地提及当前的时间(如"今天"、"现在"、"此刻"等),让说说更贴近发布时间。 + 只输出一条说说正文的内容,不要有其他的任何正文以外的冗余输出。 + """ + + # 如果有上下文,则加入到prompt中 + if context: + prompt += f"\n作为参考,这里有一些最近的聊天记录:\n---\n{context}\n---" # 添加历史记录以避免重复 prompt += "\n以下是你以前发过的说说,写新说说时注意不要在相隔不长的时间发送相同主题的说说" diff --git a/src/plugins/built_in/maizone_refactored/services/qzone_service.py b/src/plugins/built_in/maizone_refactored/services/qzone_service.py index 08dc3f085..07426ac55 100644 --- a/src/plugins/built_in/maizone_refactored/services/qzone_service.py +++ b/src/plugins/built_in/maizone_refactored/services/qzone_service.py @@ -17,7 +17,12 @@ import aiohttp import bs4 import json5 from src.common.logger import get_logger -from src.plugin_system.apis import config_api, person_api +from src.plugin_system.apis import config_api, person_api,chat_api +from src.chat.message_receive.chat_stream import get_chat_manager +from src.chat.utils.chat_message_builder import ( + build_readable_messages_with_id, + get_raw_msg_by_timestamp_with_chat, +) from .content_service import ContentService from .image_service import ImageService @@ -55,7 +60,10 @@ class QZoneService: async def send_feed(self, topic: str, stream_id: Optional[str]) -> Dict[str, Any]: """发送一条说说""" - story = await self.content_service.generate_story(topic) + # --- 获取互通组上下文 --- + context = await self._get_intercom_context(stream_id) if stream_id else None + + story = await self.content_service.generate_story(topic, context=context) if not story: return {"success": False, "message": "生成说说内容失败"} @@ -167,6 +175,65 @@ class QZoneService: # --- Internal Helper Methods --- + async def _get_intercom_context(self, stream_id: str) -> Optional[str]: + """ + 根据 stream_id 查找其所属的互通组,并构建该组的聊天上下文。 + + Args: + stream_id: 需要查找的当前聊天流ID。 + + Returns: + 如果找到匹配的组,则返回一个包含聊天记录的字符串;否则返回 None。 + """ + intercom_config = config_api.get_global_config("maizone_intercom") + if not (intercom_config and intercom_config.enable): + return None + + chat_manager = get_chat_manager() + bot_platform = config_api.get_global_config('bot.platform') + + for group in intercom_config.groups: + # 使用集合以优化查找效率 + group_stream_ids = { + chat_manager.get_stream_id(bot_platform, chat_id, True) + for chat_id in group.chat_ids + } + + if stream_id in group_stream_ids: + logger.debug(f"Stream ID '{stream_id}' 在互通组 '{getattr(group, 'name', 'Unknown')}' 中找到,正在构建上下文。") + + all_messages = [] + end_time = time.time() + start_time = end_time - (3 * 24 * 60 * 60) # 获取过去3天的消息 + + for chat_id in group.chat_ids: + # 使用正确的函数获取历史消息 + messages = get_raw_msg_by_timestamp_with_chat( + chat_id=chat_id, + timestamp_start=start_time, + timestamp_end=end_time, + limit=20, # 每个聊天最多获取20条 + limit_mode="latest" + ) + all_messages.extend(messages) + + if not all_messages: + return None + + # 按时间戳对所有消息进行排序 + all_messages.sort(key=lambda x: x.get("time", 0)) + + # 限制总消息数,例如最多100条 + if len(all_messages) > 100: + all_messages = all_messages[-100:] + + # build_readable_messages_with_id 返回一个元组 (formatted_string, message_id_list) + formatted_string, _ = build_readable_messages_with_id(all_messages) + return formatted_string + + logger.debug(f"Stream ID '{stream_id}' 未在任何互通组中找到。") + return None + async def _reply_to_own_feed_comments(self, feed: Dict, api_client: Dict): """处理对自己说说的评论并进行回复""" qq_account = config_api.get_global_config("bot.qq_account", "") @@ -290,15 +357,15 @@ class QZoneService: if cookie_data and "cookies" in cookie_data: cookie_str = cookie_data["cookies"] parsed_cookies = {k.strip(): v.strip() for k, v in (p.split('=', 1) for p in cookie_str.split('; ') if '=' in p)} - with open(cookie_file_path, "w", encoding="utf-8") as f: - orjson.dump(parsed_cookies, f) + with open(cookie_file_path, "wb") as f: + f.write(orjson.dumps(parsed_cookies)) logger.info(f"Cookie已更新并保存至: {cookie_file_path}") return parsed_cookies # 如果HTTP获取失败,尝试读取本地文件 if cookie_file_path.exists(): - with open(cookie_file_path, "r", encoding="utf-8") as f: - return orjson.loads(f) + with open(cookie_file_path, "rb") as f: + return orjson.loads(f.read()) return None except Exception as e: logger.error(f"更新或加载Cookie时发生异常: {e}") @@ -682,25 +749,23 @@ class QZoneService: # 处理不同的响应格式 json_str = "" - # 使用strip()处理可能存在的前后空白字符 stripped_res_text = res_text.strip() if stripped_res_text.startswith('_Callback(') and stripped_res_text.endswith(');'): - # JSONP格式 json_str = stripped_res_text[len('_Callback('):-2] elif stripped_res_text.startswith('{') and stripped_res_text.endswith('}'): - # 直接JSON格式 json_str = stripped_res_text else: logger.warning(f"意外的响应格式: {res_text[:100]}...") return [] - # 清理和标准化JSON字符串 json_str = json_str.replace('undefined', 'null').strip() try: json_data = json5.loads(json_str) - - # 检查API返回的错误码 + if not isinstance(json_data, dict): + logger.warning(f"解析后的JSON数据不是字典类型: {type(json_data)}") + return [] + if json_data.get('code') != 0: error_code = json_data.get('code') error_msg = json_data.get('message', '未知错误') @@ -710,6 +775,7 @@ class QZoneService: except Exception as parse_error: logger.error(f"JSON解析失败: {parse_error}, 原始数据: {json_str[:200]}...") return [] + feeds_data = [] if isinstance(json_data, dict): data_level1 = json_data.get('data') @@ -718,10 +784,9 @@ class QZoneService: feeds_list = [] for feed in feeds_data: - if not feed: + if not feed or not isinstance(feed, dict): continue - # 过滤非说说动态 if str(feed.get('appid', '')) != '311': continue @@ -730,7 +795,6 @@ class QZoneService: if not target_qq or not tid: continue - # 跳过自己的说说(监控是看好友的) if target_qq == str(uin): continue @@ -740,16 +804,14 @@ class QZoneService: soup = bs4.BeautifulSoup(html_content, 'html.parser') - # 通过点赞状态判断是否已读/处理过 like_btn = soup.find('a', class_='qz_like_btn_v3') is_liked = False - if like_btn: + if like_btn and isinstance(like_btn, bs4.Tag): is_liked = like_btn.get('data-islike') == '1' if is_liked: - continue # 如果已经点赞过,说明是已处理的说说,跳过 + continue - # 提取内容 text_div = soup.find('div', class_='f-info') text = text_div.get_text(strip=True) if text_div else "" diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index ddfe56f03..021d3ced8 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -482,9 +482,16 @@ chat_ids = [ "222222" # 假设这是“产品群”的ID ] -[[cross_context.groups]] -name = "日常闲聊组" +[maizone_intercom] +# QQ空间互通组配置 +# 启用后,发布说说时会读取指定互通组的上下文 +enable = false + +# 定义QQ空间互通组 +# 同一个组的chat_id会共享上下文,用于生成更相关的说说 +[[maizone_intercom.groups]] +name = "Maizone默认互通组" chat_ids = [ - "333333", # 假设这是“吹水群”的ID - "444444" # 假设这是“游戏群”的ID + "111111", # 示例群聊1 + "222222" # 示例群聊2 ] \ No newline at end of file