feat(maizone): 新增QQ空间互通组功能,根据聊天上下文生成说说

引入了全新的“QQ空间互通组”功能。用户可以配置多个群聊为一个互通组。
在通过指令发布说说时,系统会自动获取这些群聊的近期聊天记录作为上下文,从而生成与当前讨论话题更相关的说说内容。

- 在 `config.toml` 中新增了 `[maizone_intercom]` 配置项用于定义互通组。
- 重构并增强了动态(说说)的拉取逻辑,提高了对不同数据格式的兼容性和解析的稳定性。
- 对项目中的多个文件进行了代码清理,移除了未使用的导入,使代码更加整洁。
This commit is contained in:
minecraft1024a
2025-08-26 20:20:54 +08:00
parent cbd115efdb
commit 1e037e5ce9
13 changed files with 121 additions and 55 deletions

View File

@@ -4,7 +4,6 @@
if __name__ == "__main__": if __name__ == "__main__":
# 设置Python路径并执行bot.py # 设置Python路径并执行bot.py
import sys import sys
import os
from pathlib import Path from pathlib import Path
# 添加当前目录到Python路径 # 添加当前目录到Python路径

View File

@@ -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.base.base_command import BaseCommand
from src.plugin_system.apis.logging_api import get_logger from src.plugin_system.apis.logging_api import get_logger
from src.plugin_system.base.config_types import ConfigField 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 from src.common.message import ChatStream, Message

View File

@@ -2,7 +2,6 @@
import time import time
import re import re
import orjson import orjson
import ast
import traceback import traceback
from json_repair import repair_json from json_repair import repair_json

View File

@@ -18,8 +18,6 @@ from pathlib import Path
from typing import List, Tuple, Optional, Dict from typing import List, Tuple, Optional, Dict
import io import io
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from functools import partial
import numpy as np
from src.llm_models.utils_model import LLMRequest from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config, model_config from src.config.config import global_config, model_config

View File

@@ -47,7 +47,8 @@ from src.config.official_configs import (
WakeUpSystemConfig, WakeUpSystemConfig,
MonthlyPlanSystemConfig, MonthlyPlanSystemConfig,
CrossContextConfig, CrossContextConfig,
PermissionConfig PermissionConfig,
MaizoneIntercomConfig,
) )
from .api_ada_configs import ( from .api_ada_configs import (
@@ -396,6 +397,7 @@ class Config(ValidatedConfigBase):
wakeup_system: WakeUpSystemConfig = Field(default_factory=lambda: WakeUpSystemConfig(), description="唤醒度系统配置") wakeup_system: WakeUpSystemConfig = Field(default_factory=lambda: WakeUpSystemConfig(), description="唤醒度系统配置")
monthly_plan_system: MonthlyPlanSystemConfig = Field(default_factory=lambda: MonthlyPlanSystemConfig(), description="月层计划系统配置") monthly_plan_system: MonthlyPlanSystemConfig = Field(default_factory=lambda: MonthlyPlanSystemConfig(), description="月层计划系统配置")
cross_context: CrossContextConfig = Field(default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置") cross_context: CrossContextConfig = Field(default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置")
maizone_intercom: MaizoneIntercomConfig = Field(default_factory=lambda: MaizoneIntercomConfig(), description="Maizone互通组配置")
class APIAdapterConfig(ValidatedConfigBase): class APIAdapterConfig(ValidatedConfigBase):

View File

@@ -701,6 +701,12 @@ class CrossContextConfig(ValidatedConfigBase):
groups: List[ContextGroup] = Field(default_factory=list, description="上下文共享组列表") 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): class PermissionConfig(ValidatedConfigBase):
"""权限系统配置类""" """权限系统配置类"""

View File

@@ -3,8 +3,6 @@
import asyncio import asyncio
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List, Optional from typing import List, Optional
import orjson
from json_repair import repair_json
from src.common.database.monthly_plan_db import ( from src.common.database.monthly_plan_db import (
add_new_plans, add_new_plans,

View File

@@ -1,6 +1,5 @@
import orjson import orjson
import asyncio import asyncio
import random
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
from lunar_python import Lunar 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.sqlalchemy_models import Schedule, get_db_session
from src.common.database.monthly_plan_db import ( from src.common.database.monthly_plan_db import (
get_smart_plans_for_daily_schedule, get_smart_plans_for_daily_schedule,
update_plan_usage, update_plan_usage # 保留兼容性
soft_delete_plans # 保留兼容性
) )
from src.config.config import global_config, model_config from src.config.config import global_config, model_config
from src.llm_models.utils_model import LLMRequest from src.llm_models.utils_model import LLMRequest

View File

@@ -20,6 +20,7 @@ from src.plugin_system.apis import (
tool_api, tool_api,
permission_api, permission_api,
) )
from src.plugin_system.apis.chat_api import ChatManager as context_api
from .logging_api import get_logger from .logging_api import get_logger
from .plugin_register_api import register_plugin from .plugin_register_api import register_plugin
@@ -40,4 +41,5 @@ __all__ = [
"register_plugin", "register_plugin",
"tool_api", "tool_api",
"permission_api", "permission_api",
"context_api",
] ]

View File

@@ -10,14 +10,12 @@ from src.plugin_system.base.component_types import (
CommandInfo, CommandInfo,
EventHandlerInfo, EventHandlerInfo,
PluginInfo, PluginInfo,
EventInfo,
ComponentType, ComponentType,
) )
from src.plugin_system.base.base_command import BaseCommand from src.plugin_system.base.base_command import BaseCommand
from src.plugin_system.base.base_action import BaseAction from src.plugin_system.base.base_action import BaseAction
from src.plugin_system.base.base_tool import BaseTool 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_events_handler import BaseEventHandler
from src.plugin_system.base.base_event import BaseEvent
logger = get_logger("component_registry") logger = get_logger("component_registry")

View File

@@ -3,7 +3,7 @@
内容服务模块 内容服务模块
负责生成所有与QQ空间相关的文本内容例如说说、评论等。 负责生成所有与QQ空间相关的文本内容例如说说、评论等。
""" """
from typing import Callable from typing import Callable, Optional
import datetime import datetime
from src.common.logger import get_logger from src.common.logger import get_logger
@@ -28,11 +28,12 @@ class ContentService:
""" """
self.get_config = get_config 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 topic: 说说的主题。
:param context: 可选的聊天上下文。
:return: 生成的说说内容,如果失败则返回空字符串。 :return: 生成的说说内容,如果失败则返回空字符串。
""" """
try: try:
@@ -57,22 +58,18 @@ class ContentService:
weekday = weekday_names[now.weekday()] weekday = weekday_names[now.weekday()]
# 构建提示词 # 构建提示词
if topic: prompt_topic = f"主题是'{topic}'" if topic else "主题不限"
prompt = f""" prompt = f"""
你是'{bot_personality}',现在是{current_time}{weekday}),你想写一条主题是'{topic}'的说说发表在qq空间上 你是'{bot_personality}',现在是{current_time}{weekday}),你想写一条{prompt_topic}的说说发表在qq空间上
{bot_expression} {bot_expression}
不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,可以适当使用颜文字 不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,可以适当使用颜文字
你可以在说说中自然地提及当前的时间(如"今天""现在""此刻"等),让说说更贴近发布时间 你可以在说说中自然地提及当前的时间(如"今天""现在""此刻"等),让说说更贴近发布时间
只输出一条说说正文的内容,不要有其他的任何正文以外的冗余输出 只输出一条说说正文的内容,不要有其他的任何正文以外的冗余输出
""" """
else:
prompt = f""" # 如果有上下文则加入到prompt中
你是'{bot_personality}',现在是{current_time}{weekday}你想写一条说说发表在qq空间上主题不限 if context:
{bot_expression} prompt += f"\n作为参考,这里有一些最近的聊天记录:\n---\n{context}\n---"
不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,可以适当使用颜文字,
你可以在说说中自然地提及当前的时间(如"今天""现在""此刻"等),让说说更贴近发布时间,
只输出一条说说正文的内容,不要有其他的任何正文以外的冗余输出
"""
# 添加历史记录以避免重复 # 添加历史记录以避免重复
prompt += "\n以下是你以前发过的说说,写新说说时注意不要在相隔不长的时间发送相同主题的说说" prompt += "\n以下是你以前发过的说说,写新说说时注意不要在相隔不长的时间发送相同主题的说说"

View File

@@ -17,7 +17,12 @@ import aiohttp
import bs4 import bs4
import json5 import json5
from src.common.logger import get_logger 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 .content_service import ContentService
from .image_service import ImageService 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]: 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: if not story:
return {"success": False, "message": "生成说说内容失败"} return {"success": False, "message": "生成说说内容失败"}
@@ -167,6 +175,65 @@ class QZoneService:
# --- Internal Helper Methods --- # --- 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): async def _reply_to_own_feed_comments(self, feed: Dict, api_client: Dict):
"""处理对自己说说的评论并进行回复""" """处理对自己说说的评论并进行回复"""
qq_account = config_api.get_global_config("bot.qq_account", "") qq_account = config_api.get_global_config("bot.qq_account", "")
@@ -290,15 +357,15 @@ class QZoneService:
if cookie_data and "cookies" in cookie_data: if cookie_data and "cookies" in cookie_data:
cookie_str = cookie_data["cookies"] 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)} 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: with open(cookie_file_path, "wb") as f:
orjson.dump(parsed_cookies, f) f.write(orjson.dumps(parsed_cookies))
logger.info(f"Cookie已更新并保存至: {cookie_file_path}") logger.info(f"Cookie已更新并保存至: {cookie_file_path}")
return parsed_cookies return parsed_cookies
# 如果HTTP获取失败尝试读取本地文件 # 如果HTTP获取失败尝试读取本地文件
if cookie_file_path.exists(): if cookie_file_path.exists():
with open(cookie_file_path, "r", encoding="utf-8") as f: with open(cookie_file_path, "rb") as f:
return orjson.loads(f) return orjson.loads(f.read())
return None return None
except Exception as e: except Exception as e:
logger.error(f"更新或加载Cookie时发生异常: {e}") logger.error(f"更新或加载Cookie时发生异常: {e}")
@@ -682,25 +749,23 @@ class QZoneService:
# 处理不同的响应格式 # 处理不同的响应格式
json_str = "" json_str = ""
# 使用strip()处理可能存在的前后空白字符
stripped_res_text = res_text.strip() stripped_res_text = res_text.strip()
if stripped_res_text.startswith('_Callback(') and stripped_res_text.endswith(');'): if stripped_res_text.startswith('_Callback(') and stripped_res_text.endswith(');'):
# JSONP格式
json_str = stripped_res_text[len('_Callback('):-2] json_str = stripped_res_text[len('_Callback('):-2]
elif stripped_res_text.startswith('{') and stripped_res_text.endswith('}'): elif stripped_res_text.startswith('{') and stripped_res_text.endswith('}'):
# 直接JSON格式
json_str = stripped_res_text json_str = stripped_res_text
else: else:
logger.warning(f"意外的响应格式: {res_text[:100]}...") logger.warning(f"意外的响应格式: {res_text[:100]}...")
return [] return []
# 清理和标准化JSON字符串
json_str = json_str.replace('undefined', 'null').strip() json_str = json_str.replace('undefined', 'null').strip()
try: try:
json_data = json5.loads(json_str) json_data = json5.loads(json_str)
if not isinstance(json_data, dict):
# 检查API返回的错误码 logger.warning(f"解析后的JSON数据不是字典类型: {type(json_data)}")
return []
if json_data.get('code') != 0: if json_data.get('code') != 0:
error_code = json_data.get('code') error_code = json_data.get('code')
error_msg = json_data.get('message', '未知错误') error_msg = json_data.get('message', '未知错误')
@@ -710,6 +775,7 @@ class QZoneService:
except Exception as parse_error: except Exception as parse_error:
logger.error(f"JSON解析失败: {parse_error}, 原始数据: {json_str[:200]}...") logger.error(f"JSON解析失败: {parse_error}, 原始数据: {json_str[:200]}...")
return [] return []
feeds_data = [] feeds_data = []
if isinstance(json_data, dict): if isinstance(json_data, dict):
data_level1 = json_data.get('data') data_level1 = json_data.get('data')
@@ -718,10 +784,9 @@ class QZoneService:
feeds_list = [] feeds_list = []
for feed in feeds_data: for feed in feeds_data:
if not feed: if not feed or not isinstance(feed, dict):
continue continue
# 过滤非说说动态
if str(feed.get('appid', '')) != '311': if str(feed.get('appid', '')) != '311':
continue continue
@@ -730,7 +795,6 @@ class QZoneService:
if not target_qq or not tid: if not target_qq or not tid:
continue continue
# 跳过自己的说说(监控是看好友的)
if target_qq == str(uin): if target_qq == str(uin):
continue continue
@@ -740,16 +804,14 @@ class QZoneService:
soup = bs4.BeautifulSoup(html_content, 'html.parser') soup = bs4.BeautifulSoup(html_content, 'html.parser')
# 通过点赞状态判断是否已读/处理过
like_btn = soup.find('a', class_='qz_like_btn_v3') like_btn = soup.find('a', class_='qz_like_btn_v3')
is_liked = False is_liked = False
if like_btn: if like_btn and isinstance(like_btn, bs4.Tag):
is_liked = like_btn.get('data-islike') == '1' is_liked = like_btn.get('data-islike') == '1'
if is_liked: if is_liked:
continue # 如果已经点赞过,说明是已处理的说说,跳过 continue
# 提取内容
text_div = soup.find('div', class_='f-info') text_div = soup.find('div', class_='f-info')
text = text_div.get_text(strip=True) if text_div else "" text = text_div.get_text(strip=True) if text_div else ""

View File

@@ -482,9 +482,16 @@ chat_ids = [
"222222" # 假设这是“产品群”的ID "222222" # 假设这是“产品群”的ID
] ]
[[cross_context.groups]] [maizone_intercom]
name = "日常闲聊组" # QQ空间互通组配置
# 启用后,发布说说时会读取指定互通组的上下文
enable = false
# 定义QQ空间互通组
# 同一个组的chat_id会共享上下文用于生成更相关的说说
[[maizone_intercom.groups]]
name = "Maizone默认互通组"
chat_ids = [ chat_ids = [
"333333", # 假设这是“吹水群”的ID "111111", # 示例群聊1
"444444" # 假设这是“游戏群”的ID "222222" # 示例群聊2
] ]