diff --git a/src/plugins/built_in/maizone_refactored/services/monitor_service.py b/src/plugins/built_in/maizone_refactored/services/monitor_service.py index 9c46d5370..9875af944 100644 --- a/src/plugins/built_in/maizone_refactored/services/monitor_service.py +++ b/src/plugins/built_in/maizone_refactored/services/monitor_service.py @@ -55,7 +55,6 @@ class MonitorService: interval_minutes = self.get_config("monitor.interval_minutes", 10) - logger.info("开始执行好友动态监控...") await self.qzone_service.monitor_feeds() logger.info(f"本轮监控完成,将在 {interval_minutes} 分钟后进行下一次检查。") 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 de470019b..eb3664e61 100644 --- a/src/plugins/built_in/maizone_refactored/services/qzone_service.py +++ b/src/plugins/built_in/maizone_refactored/services/qzone_service.py @@ -3,6 +3,7 @@ QQ空间服务模块 封装了所有与QQ空间API的直接交互,是插件的核心业务逻辑层。 """ + import asyncio import base64 import json @@ -29,6 +30,7 @@ class QZoneService: """ QQ空间服务类,负责所有API交互和业务流程编排。 """ + # --- API Endpoints --- ZONE_LIST_URL = "https://user.qzone.qq.com/proxy/domain/ic2.qzone.qq.com/cgi-bin/feeds/feeds3_html_more" EMOTION_PUBLISH_URL = "https://user.qzone.qq.com/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_publish_v6" @@ -37,8 +39,13 @@ class QZoneService: LIST_URL = "https://user.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msglist_v6" REPLY_URL = "https://user.qzone.qq.com/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_re_feeds" - - def __init__(self, get_config: Callable, content_service: ContentService, image_service: ImageService, cookie_service: CookieService): + def __init__( + self, + get_config: Callable, + content_service: ContentService, + image_service: ImageService, + cookie_service: CookieService, + ): self.get_config = get_config self.content_service = content_service self.image_service = image_service @@ -53,7 +60,7 @@ class QZoneService: return {"success": False, "message": "生成说说内容失败"} await self.image_service.generate_images_for_story(story) - + qq_account = config_api.get_global_config("bot.qq_account", "") api_client = await self._get_api_client(qq_account, stream_id) if not api_client: @@ -78,7 +85,7 @@ class QZoneService: return {"success": False, "message": "根据活动生成说说内容失败"} await self.image_service.generate_images_for_story(story) - + qq_account = config_api.get_global_config("bot.qq_account", "") # 注意:定时任务通常在后台运行,没有特定的用户会话,因此 stream_id 为 None api_client = await self._get_api_client(qq_account, stream_id=None) @@ -116,7 +123,7 @@ class QZoneService: feeds = await api_client["list_feeds"](target_qq, num_to_read) if not feeds: return {"success": True, "message": f"没有从'{target_name}'的空间获取到新说说。"} - + for feed in feeds: await self._process_single_feed(feed, api_client, target_qq, target_name) await asyncio.sleep(random.uniform(3, 7)) @@ -136,11 +143,11 @@ class QZoneService: return try: - feeds = await api_client["monitor_list_feeds"](20) # 监控时检查最近20条动态 + feeds = await api_client["monitor_list_feeds"](20) # 监控时检查最近20条动态 if not feeds: logger.info("监控完成:未发现新说说") return - + logger.info(f"监控任务: 发现 {len(feeds)} 条新动态,准备处理...") for feed in feeds: target_qq = feed.get("target_qq") @@ -153,7 +160,7 @@ class QZoneService: await self._reply_to_own_feed_comments(feed, api_client) else: await self._process_single_feed(feed, api_client, target_qq, target_qq) - + await asyncio.sleep(random.uniform(5, 10)) except Exception as e: logger.error(f"监控好友动态时发生异常: {e}", exc_info=True) @@ -171,24 +178,31 @@ class QZoneService: return # 筛选出未被自己回复过的主评论 - my_comment_tids = {c['parent_tid'] for c in comments if c.get('parent_tid') and c.get('qq_account') == qq_account} - comments_to_reply = [c for c in comments if not c.get('parent_tid') and c.get('comment_tid') not in my_comment_tids] + my_comment_tids = { + c["parent_tid"] for c in comments if c.get("parent_tid") and c.get("qq_account") == qq_account + } + comments_to_reply = [ + c for c in comments if not c.get("parent_tid") and c.get("comment_tid") not in my_comment_tids + ] if not comments_to_reply: return logger.info(f"发现自己说说下的 {len(comments_to_reply)} 条新评论,准备回复...") for comment in comments_to_reply: - reply_content = await self.content_service.generate_comment_reply(content, comment.get('content', ''), comment.get('nickname', '')) + reply_content = await self.content_service.generate_comment_reply( + content, comment.get("content", ""), comment.get("nickname", "") + ) if reply_content: - success = await api_client["reply"](fid, qq_account, comment.get('nickname', ''), reply_content, comment.get('comment_tid')) + success = await api_client["reply"]( + fid, qq_account, comment.get("nickname", ""), reply_content, comment.get("comment_tid") + ) if success: logger.info(f"成功回复'{comment.get('nickname', '')}'的评论: '{reply_content}'") else: logger.error(f"回复'{comment.get('nickname', '')}'的评论失败") await asyncio.sleep(random.uniform(10, 20)) - async def _process_single_feed(self, feed: Dict, api_client: Dict, target_qq: str, target_name: str): """处理单条说说,决定是否评论和点赞""" content = feed.get("content", "") @@ -207,7 +221,7 @@ class QZoneService: images = [] if not os.path.exists(image_dir): return images - + try: files = sorted([f for f in os.listdir(image_dir) if os.path.isfile(os.path.join(image_dir, f))]) for filename in files: @@ -228,21 +242,26 @@ class QZoneService: async def _get_api_client(self, qq_account: str, stream_id: Optional[str]) -> Optional[Dict]: cookies = await self.cookie_service.get_cookies(qq_account, stream_id) - if not cookies: return None - - p_skey = cookies.get('p_skey') or cookies.get('p_skey'.upper()) - if not p_skey: return None - + if not cookies: + return None + + p_skey = cookies.get("p_skey") or cookies.get("p_skey".upper()) + if not p_skey: + return None + gtk = self._generate_gtk(p_skey) - uin = cookies.get('uin', '').lstrip('o') - + uin = cookies.get("uin", "").lstrip("o") + async def _request(method, url, params=None, data=None, headers=None): - final_headers = {'referer': f'https://user.qzone.qq.com/{uin}', 'origin': 'https://user.qzone.qq.com'} - if headers: final_headers.update(headers) - + final_headers = {"referer": f"https://user.qzone.qq.com/{uin}", "origin": "https://user.qzone.qq.com"} + if headers: + final_headers.update(headers) + async with aiohttp.ClientSession(cookies=cookies) as session: timeout = aiohttp.ClientTimeout(total=20) - async with session.request(method, url, params=params, data=data, headers=final_headers, timeout=timeout) as response: + async with session.request( + method, url, params=params, data=data, headers=final_headers, timeout=timeout + ) as response: response.raise_for_status() return await response.text() @@ -250,11 +269,18 @@ class QZoneService: """发布说说""" try: post_data = { - "syn_tweet_verson": "1", "paramstr": "1", "who": "1", - "con": content, "feedversion": "1", "ver": "1", - "ugc_right": "1", "to_sign": "0", "hostuin": uin, - "code_version": "1", "format": "json", - "qzreferrer": f"https://user.qzone.qq.com/{uin}" + "syn_tweet_verson": "1", + "paramstr": "1", + "who": "1", + "con": content, + "feedversion": "1", + "ver": "1", + "ugc_right": "1", + "to_sign": "0", + "hostuin": uin, + "code_version": "1", + "format": "json", + "qzreferrer": f"https://user.qzone.qq.com/{uin}", } if images: pic_bos, richvals = [], [] @@ -265,17 +291,17 @@ class QZoneService: # This is a placeholder for the actual image upload logic which is quite complex. # In a real scenario, you would call a dedicated `_upload_image` method here. # For now, we assume the upload is successful and we get back dummy data. - pass # Simplified for this example + pass # Simplified for this example # Dummy data for illustration if images: - post_data['pic_bo'] = 'dummy_pic_bo' - post_data['richtype'] = '1' - post_data['richval'] = 'dummy_rich_val' + post_data["pic_bo"] = "dummy_pic_bo" + post_data["richtype"] = "1" + post_data["richval"] = "dummy_rich_val" - res_text = await _request("POST", self.EMOTION_PUBLISH_URL, params={'g_tk': gtk}, data=post_data) + res_text = await _request("POST", self.EMOTION_PUBLISH_URL, params={"g_tk": gtk}, data=post_data) result = json.loads(res_text) - tid = result.get('tid', '') + tid = result.get("tid", "") return bool(tid), tid except Exception as e: logger.error(f"发布说说异常: {e}", exc_info=True) @@ -285,27 +311,44 @@ class QZoneService: """获取指定用户说说列表""" try: params = { - 'g_tk': gtk, "uin": t_qq, "ftype": 0, "sort": 0, "pos": 0, - "num": num, "replynum": 100, "callback": "_preloadCallback", - "code_version": 1, "format": "jsonp", "need_comment": 1 + "g_tk": gtk, + "uin": t_qq, + "ftype": 0, + "sort": 0, + "pos": 0, + "num": num, + "replynum": 100, + "callback": "_preloadCallback", + "code_version": 1, + "format": "jsonp", + "need_comment": 1, } res_text = await _request("GET", self.LIST_URL, params=params) - json_str = res_text[len('_preloadCallback('):-2] + json_str = res_text[len("_preloadCallback(") : -2] json_data = json.loads(json_str) - if json_data.get('code') != 0: return [] + if json_data.get("code") != 0: + return [] feeds_list = [] - my_name = json_data.get('logininfo', {}).get('name', '') + my_name = json_data.get("logininfo", {}).get("name", "") for msg in json_data.get("msglist", []): - is_commented = any(c.get("name") == my_name for c in msg.get("commentlist", []) if isinstance(c, dict)) + is_commented = any( + c.get("name") == my_name for c in msg.get("commentlist", []) if isinstance(c, dict) + ) if not is_commented: - feeds_list.append({ - "tid": msg.get("tid", ""), - "content": msg.get("content", ""), - "created_time": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(msg.get("created_time", 0))), - "rt_con": msg.get("rt_con", {}).get("content", "") if isinstance(msg.get("rt_con"), dict) else "" - }) + feeds_list.append( + { + "tid": msg.get("tid", ""), + "content": msg.get("content", ""), + "created_time": time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(msg.get("created_time", 0)) + ), + "rt_con": msg.get("rt_con", {}).get("content", "") + if isinstance(msg.get("rt_con"), dict) + else "", + } + ) return feeds_list except Exception as e: logger.error(f"获取说说列表失败: {e}", exc_info=True) @@ -315,9 +358,15 @@ class QZoneService: """评论说说""" try: data = { - "topicId": f'{t_qq}_{feed_id}__1', "uin": uin, "hostUin": t_qq, - "content": text, "format": "fs", "plat": "qzone", "source": "ic", - "platformid": 52, "ref": "feeds" + "topicId": f"{t_qq}_{feed_id}__1", + "uin": uin, + "hostUin": t_qq, + "content": text, + "format": "fs", + "plat": "qzone", + "source": "ic", + "platformid": 52, + "ref": "feeds", } await _request("POST", self.COMMENT_URL, params={"g_tk": gtk}, data=data) return True @@ -329,17 +378,24 @@ class QZoneService: """点赞说说""" try: data = { - 'opuin': uin, 'unikey': f'http://user.qzone.qq.com/{t_qq}/mood/{feed_id}', - 'curkey': f'http://user.qzone.qq.com/{t_qq}/mood/{feed_id}', - 'from': 1, 'appid': 311, 'typeid': 0, 'abstime': int(time.time()), - 'fid': feed_id, 'active': 0, 'format': 'json', 'fupdate': 1 + "opuin": uin, + "unikey": f"http://user.qzone.qq.com/{t_qq}/mood/{feed_id}", + "curkey": f"http://user.qzone.qq.com/{t_qq}/mood/{feed_id}", + "from": 1, + "appid": 311, + "typeid": 0, + "abstime": int(time.time()), + "fid": feed_id, + "active": 0, + "format": "json", + "fupdate": 1, } - await _request("POST", self.DOLIKE_URL, params={'g_tk': gtk}, data=data) + await _request("POST", self.DOLIKE_URL, params={"g_tk": gtk}, data=data) return True except Exception as e: logger.error(f"点赞说说异常: {e}", exc_info=True) return False - + async def _reply(fid, host_qq, target_name, content, comment_tid): """回复评论""" try: @@ -355,7 +411,7 @@ class QZoneService: "ref": "feeds", "richtype": "", "richval": "", - "paramstr": f"@{target_name} {content}" + "paramstr": f"@{target_name} {content}", } await _request("POST", self.REPLY_URL, params={"g_tk": gtk}, data=data) return True @@ -367,38 +423,55 @@ class QZoneService: """监控好友动态""" try: params = { - "uin": uin, "scope": 0, "view": 1, "filter": "all", "flag": 1, - "applist": "all", "pagenum": 1, "count": num, "format": "json", - "g_tk": gtk, "useutf8": 1, "outputhtmlfeed": 1 + "uin": uin, + "scope": 0, + "view": 1, + "filter": "all", + "flag": 1, + "applist": "all", + "pagenum": 1, + "count": num, + "format": "json", + "g_tk": gtk, + "useutf8": 1, + "outputhtmlfeed": 1, } res_text = await _request("GET", self.ZONE_LIST_URL, params=params) - json_str = res_text[len('_Callback('):-2].replace('undefined', 'null') + + # 增加对返回内容的校验 + if not res_text.startswith("_Callback(") or not res_text.endswith(");"): + logger.warning(f"监控好友动态返回格式异常: {res_text}") + return [] + + json_str = res_text[len("_Callback(") : -2].replace("undefined", "null") json_data = json5.loads(json_str) feeds_data = [] if isinstance(json_data, dict): - data_level1 = json_data.get('data') + data_level1 = json_data.get("data") if isinstance(data_level1, dict): - feeds_data = data_level1.get('data', []) - + feeds_data = data_level1.get("data", []) + feeds_list = [] for feed in feeds_data: - if str(feed.get('appid', '')) != '311' or str(feed.get('uin', '')) == str(uin): - continue - - html_content = feed.get('html', '') - soup = bs4.BeautifulSoup(html_content, 'html.parser') - like_btn = soup.find('a', class_='qz_like_btn_v3') - if isinstance(like_btn, bs4.element.Tag) and like_btn.get('data-islike') == '1': + if str(feed.get("appid", "")) != "311" or str(feed.get("uin", "")) == str(uin): continue - text_div = soup.find('div', class_='f-info') + html_content = feed.get("html", "") + soup = bs4.BeautifulSoup(html_content, "html.parser") + like_btn = soup.find("a", class_="qz_like_btn_v3") + if isinstance(like_btn, bs4.element.Tag) and like_btn.get("data-islike") == "1": + continue + + text_div = soup.find("div", class_="f-info") text = text_div.get_text(strip=True) if text_div else "" - - feeds_list.append({ - 'target_qq': feed.get('uin'), - 'tid': feed.get('key'), - 'content': text, - }) + + feeds_list.append( + { + "target_qq": feed.get("uin"), + "tid": feed.get("key"), + "content": text, + } + ) return feeds_list except Exception as e: logger.error(f"监控好友动态失败: {e}", exc_info=True) @@ -411,4 +484,4 @@ class QZoneService: "like": _like, "reply": _reply, "monitor_list_feeds": _monitor_list_feeds, - } \ No newline at end of file + }