diff --git a/admin/dashboard/server.py b/admin/dashboard/server.py index 7b6e879..6eb767a 100644 --- a/admin/dashboard/server.py +++ b/admin/dashboard/server.py @@ -178,12 +178,12 @@ class DashboardServer: return {"success": False, "message": "WCF实例不可用"} # 获取当前登录的微信ID - wx_id = self.wcf.get_self_wxid() + wx_id = self.self.message_util.get_self_wxid() if not wx_id: return {"success": False, "message": "未获取到微信ID"} # 获取用户详细信息 - user_info = self.wcf.get_user_info() + user_info = self.self.message_util.get_user_info() self.logger.info(f"获取用户信息:{user_info}") return { diff --git a/base/chatglm/README.MD b/base/chatglm/README.MD deleted file mode 100644 index 852aa16..0000000 --- a/base/chatglm/README.MD +++ /dev/null @@ -1,45 +0,0 @@ -# ChatGLM3 集成使用说明 - -1. 需要取消配置中 chatglm 的注释, 并配置对应信息,使用 [ChatGLM3](https://github.com/THUDM/ChatGLM3), 启用最新版 ChatGLM3 根目录下 openai_api.py 获取 api 地址: -```yaml -# 如果要使用 chatglm,取消下面的注释并填写相关内容 -chatglm: - key: sk-012345678901234567890123456789012345678901234567 # 根据需要自己做key校验 - api: http://localhost:8000/v1 # 根据自己的chatglm地址修改 - proxy: # 如果你在国内,你可能需要魔法,大概长这样:http://域名或者IP地址:端口号 - prompt: 你是智能聊天机器人,你叫小薇 # 根据需要对角色进行设定 - file_path: F:/Pictures/temp #设定生成图片和代码使用的文件夹路径 -``` - -2. 修改 chatglm/tool_registry.py 工具里面的一下配置,comfyUI 地址或者根据需要自己配置一些工具,函数名上需要加 @register_tool, 函数里面需要叫'''函数描述''',参数需要用 Annotated[str,'',True] 修饰,分别是类型,参数说明,是否必填,再加 ->加上对应的返回类型 -```python -@register_tool -def get_confyui_image(prompt: Annotated[str, '要生成图片的提示词,注意必须是英文', True]) -> dict: - ''' - 生成图片 - ''' - with open("func_chatglm\\base.json", "r", encoding="utf-8") as f: - data2 = json.load(f) - data2['prompt']['3']['inputs']['seed'] = ''.join( - random.sample('123456789012345678901234567890', 14)) - # 模型名称 - data2['prompt']['4']['inputs']['ckpt_name'] = 'chilloutmix_NiPrunedFp32Fix.safetensors' - data2['prompt']['6']['inputs']['text'] = prompt # 正向提示词 - # data2['prompt']['7']['inputs']['text']='' #反向提示词 - cfui = ComfyUIApi(server_address="127.0.0.1:8188") # 根据自己comfyUI地址修改 - images = cfui.get_images(data2['prompt']) - return {'res': images[0]['image'], 'res_type': 'image', 'filename': images[0]['filename']} - -``` - -3. 使用 Code Interpreter 还需要安装 Jupyter 内核,默认名称叫 chatglm3: -``` -ipython kernel install --name chatglm3 --user -``` - -如果名称需要自定义,可以配置系统环境变量:IPYKERNEL 或者修改 chatglm/code_kernel.py -``` -IPYKERNEL = os.environ.get('IPYKERNEL', 'chatglm3') -``` - -4. 启动后,发送 #帮助 可以查看 模式和常用指令 diff --git a/base/chatglm/__init__.py b/base/chatglm/__init__.py deleted file mode 100644 index fede0f3..0000000 --- a/base/chatglm/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -import sys - - -class UnsupportedPythonVersionError(Exception): - def __init__(self, error_msg: str): - super().__init__(error_msg) - - -python_version_info = sys.version_info -if not sys.version_info >= (3, 9): - msg = "当前Python版本: " + ".".join(map(str, python_version_info[:3])) + (', 需要python版本 >= 3.9, 前往下载: ' - 'https://www.python.org/downloads/') - raise UnsupportedPythonVersionError(msg) \ No newline at end of file diff --git a/base/chatglm/base.json b/base/chatglm/base.json deleted file mode 100644 index 6bfcd3f..0000000 --- a/base/chatglm/base.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "prompt": { - "3": { - "inputs": { - "seed": 1000573256060686, - "steps": 20, - "cfg": 8, - "sampler_name": "euler", - "scheduler": "normal", - "denoise": 1, - "model": [ - "4", - 0 - ], - "positive": [ - "6", - 0 - ], - "negative": [ - "7", - 0 - ], - "latent_image": [ - "5", - 0 - ] - }, - "class_type": "KSampler" - }, - "4": { - "inputs": { - "ckpt_name": "(修复)512-inpainting-ema.safetensors" - }, - "class_type": "CheckpointLoaderSimple" - }, - "5": { - "inputs": { - "width": 512, - "height": 512, - "batch_size": 1 - }, - "class_type": "EmptyLatentImage" - }, - "6": { - "inputs": { - "text": "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,dress, ", - "clip": [ - "4", - 1 - ] - }, - "class_type": "CLIPTextEncode" - }, - "7": { - "inputs": { - "text": "text, watermark", - "clip": [ - "4", - 1 - ] - }, - "class_type": "CLIPTextEncode" - }, - "8": { - "inputs": { - "samples": [ - "3", - 0 - ], - "vae": [ - "4", - 2 - ] - }, - "class_type": "VAEDecode" - }, - "9": { - "inputs": { - "filename_prefix": "ComfyUI", - "images": [ - "8", - 0 - ] - }, - "class_type": "SaveImage" - } - } -} \ No newline at end of file diff --git a/base/chatglm/code_kernel.py b/base/chatglm/code_kernel.py deleted file mode 100644 index 3cbd273..0000000 --- a/base/chatglm/code_kernel.py +++ /dev/null @@ -1,199 +0,0 @@ -import base64 -import os -import queue -import re -from io import BytesIO -from subprocess import PIPE -from typing import Optional, Union - -import jupyter_client -from PIL import Image - -IPYKERNEL = os.environ.get('IPYKERNEL', 'chatglm3') - - -class CodeKernel(object): - def __init__(self, - kernel_name='kernel', - kernel_id=None, - kernel_config_path="", - python_path=None, - ipython_path=None, - init_file_path="./startup.py", - verbose=1): - - self.kernel_name = kernel_name - self.kernel_id = kernel_id - self.kernel_config_path = kernel_config_path - self.python_path = python_path - self.ipython_path = ipython_path - self.init_file_path = init_file_path - self.verbose = verbose - - if python_path is None and ipython_path is None: - env = None - else: - env = {"PATH": self.python_path + ":$PATH", - "PYTHONPATH": self.python_path} - - # Initialize the backend kernel - self.kernel_manager = jupyter_client.KernelManager(kernel_name=IPYKERNEL, - connection_file=self.kernel_config_path, - exec_files=[ - self.init_file_path], - env=env) - if self.kernel_config_path: - self.kernel_manager.load_connection_file() - self.kernel_manager.start_kernel(stdout=PIPE, stderr=PIPE) - print("Backend kernel started with the configuration: {}".format( - self.kernel_config_path)) - else: - self.kernel_manager.start_kernel(stdout=PIPE, stderr=PIPE) - print("Backend kernel started with the configuration: {}".format( - self.kernel_manager.connection_file)) - - if verbose: - print(self.kernel_manager.get_connection_info()) - - # Initialize the code kernel - self.kernel = self.kernel_manager.blocking_client() - # self.kernel.load_connection_file() - self.kernel.start_channels() - print("Code kernel started.") - - def execute(self, code): - self.kernel.execute(code) - try: - shell_msg = self.kernel.get_shell_msg(timeout=40) - io_msg_content = self.kernel.get_iopub_msg(timeout=40)['content'] - while True: - msg_out = io_msg_content - # Poll the message - try: - io_msg_content = self.kernel.get_iopub_msg(timeout=40)[ - 'content'] - if 'execution_state' in io_msg_content and io_msg_content['execution_state'] == 'idle': - break - except queue.Empty: - break - - return shell_msg, msg_out - except Exception as e: - print(e) - return None - - def execute_interactive(self, code, verbose=False): - shell_msg = self.kernel.execute_interactive(code) - if shell_msg is queue.Empty: - if verbose: - print("Timeout waiting for shell message.") - self.check_msg(shell_msg, verbose=verbose) - - return shell_msg - - def inspect(self, code, verbose=False): - msg_id = self.kernel.inspect(code) - shell_msg = self.kernel.get_shell_msg(timeout=30) - if shell_msg is queue.Empty: - if verbose: - print("Timeout waiting for shell message.") - self.check_msg(shell_msg, verbose=verbose) - - return shell_msg - - def get_error_msg(self, msg, verbose=False) -> Optional[str]: - if msg['content']['status'] == 'error': - try: - error_msg = msg['content']['traceback'] - except BaseException: - try: - error_msg = msg['content']['traceback'][-1].strip() - except BaseException: - error_msg = "Traceback Error" - if verbose: - print("Error: ", error_msg) - return error_msg - return None - - def check_msg(self, msg, verbose=False): - status = msg['content']['status'] - if status == 'ok': - if verbose: - print("Execution succeeded.") - elif status == 'error': - for line in msg['content']['traceback']: - if verbose: - print(line) - - def shutdown(self): - # Shutdown the backend kernel - self.kernel_manager.shutdown_kernel() - print("Backend kernel shutdown.") - # Shutdown the code kernel - self.kernel.shutdown() - print("Code kernel shutdown.") - - def restart(self): - # Restart the backend kernel - self.kernel_manager.restart_kernel() - # print("Backend kernel restarted.") - - def interrupt(self): - # Interrupt the backend kernel - self.kernel_manager.interrupt_kernel() - # print("Backend kernel interrupted.") - - def is_alive(self): - return self.kernel.is_alive() - - -def b64_2_img(data): - buff = BytesIO(base64.b64decode(data)) - return Image.open(buff) - - -def clean_ansi_codes(input_string): - ansi_escape = re.compile(r'(\x9B|\x1B\[|\u001b\[)[0-?]*[ -/]*[@-~]') - return ansi_escape.sub('', input_string) - - -def execute(code, kernel: CodeKernel) -> tuple[str, Union[str, Image.Image]]: - res = "" - res_type = None - code = code.replace("<|observation|>", "") - code = code.replace("<|assistant|>interpreter", "") - code = code.replace("<|assistant|>", "") - code = code.replace("<|user|>", "") - code = code.replace("<|system|>", "") - msg, output = kernel.execute(code) - - if msg['metadata']['status'] == "timeout": - return res_type, 'Timed out' - elif msg['metadata']['status'] == 'error': - return res_type, clean_ansi_codes('\n'.join(kernel.get_error_msg(msg, verbose=True))) - - if 'text' in output: - res_type = "text" - res = output['text'] - elif 'data' in output: - for key in output['data']: - if 'image/png' in key: - res_type = "image" - res = output['data'][key] - break - elif 'text/plain' in key: - res_type = "text" - res = output['data'][key] - - if res_type == "image": - return res_type, b64_2_img(res) - elif res_type == "text" or res_type == "traceback": - res = res - - return res_type, res - - -def extract_code(text: str) -> str: - pattern = r'```([^\n]*)\n(.*?)```' - matches = re.findall(pattern, text, re.DOTALL) - return matches[-1][1] diff --git a/base/chatglm/comfyUI_api.py b/base/chatglm/comfyUI_api.py deleted file mode 100644 index aefe6cc..0000000 --- a/base/chatglm/comfyUI_api.py +++ /dev/null @@ -1,186 +0,0 @@ -# This is an example that uses the websockets api to know when a prompt execution is done -# Once the prompt execution is done it downloads the images using the /history endpoint - -import io -import json -import random -import urllib -import uuid - -import requests -# NOTE: websocket-client (https://github.com/websocket-client/websocket-client) -import websocket -from PIL import Image - - -class ComfyUIApi(): - def __init__(self, server_address="127.0.0.1:8188"): - self.server_address = server_address - self.client_id = str(uuid.uuid4()) - self.ws = websocket.WebSocket() - self.ws.connect( - "ws://{}/ws?clientId={}".format(server_address, self.client_id)) - - def queue_prompt(self, prompt): - p = {"prompt": prompt, "client_id": self.client_id} - data = json.dumps(p).encode('utf-8') - req = requests.post( - "http://{}/prompt".format(self.server_address), data=data) - print(req.text) - return json.loads(req.text) - - def get_image(self, filename, subfolder, folder_type): - data = {"filename": filename, - "subfolder": subfolder, "type": folder_type} - url_values = urllib.parse.urlencode(data) - with requests.get("http://{}/view?{}".format(self.server_address, url_values)) as response: - image = Image.open(io.BytesIO(response.content)) - return image - - def get_image_url(self, filename, subfolder, folder_type): - data = {"filename": filename, - "subfolder": subfolder, "type": folder_type} - url_values = urllib.parse.urlencode(data) - return "http://{}/view?{}".format(self.server_address, url_values) - - def get_history(self, prompt_id): - with requests.get("http://{}/history/{}".format(self.server_address, prompt_id)) as response: - return json.loads(response.text) - - def get_images(self, prompt, isUrl=False): - prompt_id = self.queue_prompt(prompt)['prompt_id'] - output_images = [] - while True: - out = self.ws.recv() - if isinstance(out, str): - message = json.loads(out) - if message['type'] == 'executing': - data = message['data'] - if data['node'] is None and data['prompt_id'] == prompt_id: - break # Execution is done - else: - continue # previews are binary data - - history = self.get_history(prompt_id)[prompt_id] - for o in history['outputs']: - for node_id in history['outputs']: - node_output = history['outputs'][node_id] - if 'images' in node_output: - for image in node_output['images']: - image_data = self.get_image_url(image['filename'], image['subfolder'], image['type']) if isUrl else self.get_image( - image['filename'], image['subfolder'], image['type']) - image['image'] = image_data - output_images.append(image) - - return output_images - - -prompt_text = """ -{ - "3": { - "class_type": "KSampler", - "inputs": { - "cfg": 8, - "denoise": 1, - "latent_image": [ - "5", - 0 - ], - "model": [ - "4", - 0 - ], - "negative": [ - "7", - 0 - ], - "positive": [ - "6", - 0 - ], - "sampler_name": "euler", - "scheduler": "normal", - "seed": 8566257, - "steps": 20 - } - }, - "4": { - "class_type": "CheckpointLoaderSimple", - "inputs": { - "ckpt_name": "chilloutmix_NiPrunedFp32Fix.safetensors" - } - }, - "5": { - "class_type": "EmptyLatentImage", - "inputs": { - "batch_size": 1, - "height": 512, - "width": 512 - } - }, - "6": { - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "masterpiece best quality girl" - } - }, - "7": { - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "bad hands" - } - }, - "8": { - "class_type": "VAEDecode", - "inputs": { - "samples": [ - "3", - 0 - ], - "vae": [ - "4", - 2 - ] - } - }, - "9": { - "class_type": "SaveImage", - "inputs": { - "filename_prefix": "ComfyUI", - "images": [ - "8", - 0 - ] - } - } -} -""" -if __name__ == '__main__': - prompt = json.loads(prompt_text) - # set the text prompt for our positive CLIPTextEncode - prompt["6"]["inputs"]["text"] = "masterpiece best quality man" - - # set the seed for our KSampler node - prompt["3"]["inputs"]["seed"] = ''.join( - random.sample('123456789012345678901234567890', 14)) - - cfui = ComfyUIApi() - images = cfui.get_images(prompt) - - # Commented out code to display the output images: - - for node_id in images: - for image_data in images[node_id]: - import io - - from PIL import Image - image = Image.open(io.BytesIO(image_data)) - image.show() diff --git a/base/chatglm/tool_registry.py b/base/chatglm/tool_registry.py deleted file mode 100644 index 27279df..0000000 --- a/base/chatglm/tool_registry.py +++ /dev/null @@ -1,167 +0,0 @@ -import inspect -import json -import random -import re -import traceback -from copy import deepcopy -from datetime import datetime -from types import GenericAlias -from typing import Annotated, get_origin - -from base.chatglm.comfyUI_api import ComfyUIApi -from base.func_news import News -from zhdate import ZhDate - -_TOOL_HOOKS = {} -_TOOL_DESCRIPTIONS = {} - - -def extract_code(text: str) -> str: - pattern = r'```([^\n]*)\n(.*?)```' - matches = re.findall(pattern, text, re.DOTALL) - return matches[-1][1] - - -def register_tool(func: callable): - tool_name = func.__name__ - tool_description = inspect.getdoc(func).strip() - python_params = inspect.signature(func).parameters - tool_params = [] - for name, param in python_params.items(): - annotation = param.annotation - if annotation is inspect.Parameter.empty: - raise TypeError(f"Parameter `{name}` missing type annotation") - if get_origin(annotation) != Annotated: - raise TypeError( - f"Annotation type for `{name}` must be typing.Annotated") - - typ, (description, required) = annotation.__origin__, annotation.__metadata__ - typ: str = str(typ) if isinstance(typ, GenericAlias) else typ.__name__ - if not isinstance(description, str): - raise TypeError(f"Description for `{name}` must be a string") - if not isinstance(required, bool): - raise TypeError(f"Required for `{name}` must be a bool") - - tool_params.append({ - "name": name, - "description": description, - "type": typ, - "required": required - }) - tool_def = { - "name": tool_name, - "description": tool_description, - "parameters": tool_params - } - - # print("[registered tool] " + pformat(tool_def)) - _TOOL_HOOKS[tool_name] = func - _TOOL_DESCRIPTIONS[tool_name] = tool_def - - return func - - -def dispatch_tool(tool_name: str, tool_params: dict) -> str: - if tool_name not in _TOOL_HOOKS: - return f"Tool `{tool_name}` not found. Please use a provided tool." - tool_call = _TOOL_HOOKS[tool_name] - try: - ret = tool_call(**tool_params) - except BaseException: - ret = traceback.format_exc() - return ret - - -def get_tools() -> dict: - return deepcopy(_TOOL_DESCRIPTIONS) - -# Tool Definitions - -# @register_tool -# def random_number_generator( -# seed: Annotated[int, 'The random seed used by the generator', True], -# range: Annotated[tuple[int, int], 'The range of the generated numbers', True], -# ) -> int: -# """ -# Generates a random number x, s.t. range[0] <= x < range[1] -# """ -# if not isinstance(seed, int): -# raise TypeError("Seed must be an integer") -# if not isinstance(range, tuple): -# raise TypeError("Range must be a tuple") -# if not isinstance(range[0], int) or not isinstance(range[1], int): -# raise TypeError("Range must be a tuple of integers") - -# import random -# return random.Random(seed).randint(*range) - - -@register_tool -def get_weather( - city_name: Annotated[str, 'The name of the city to be queried', True], -) -> str: - """ - Get the current weather for `city_name` - """ - if not isinstance(city_name, str): - raise TypeError("City name must be a string") - - key_selection = { - "current_condition": ["temp_C", "FeelsLikeC", "humidity", "weatherDesc", "observation_time"], - } - import requests - try: - resp = requests.get(f"https://wttr.in/{city_name}?format=j1") - resp.raise_for_status() - resp = resp.json() - ret = {k: {_v: resp[k][0][_v] for _v in v} - for k, v in key_selection.items()} - except BaseException: - import traceback - ret = "Error encountered while fetching weather data!\n" + traceback.format_exc() - - return str(ret) - - -@register_tool -def get_confyui_image(prompt: Annotated[str, '要生成图片的提示词,注意必须是英文', True]) -> dict: - ''' - 生成图片 - ''' - with open("chatglm\\base.json", "r", encoding="utf-8") as f: - data2 = json.load(f) - data2['prompt']['3']['inputs']['seed'] = ''.join( - random.sample('123456789012345678901234567890', 14)) - # 模型名称 - data2['prompt']['4']['inputs']['ckpt_name'] = 'chilloutmix_NiPrunedFp32Fix.safetensors' - data2['prompt']['6']['inputs']['text'] = prompt # 正向提示词 - # data2['prompt']['7']['inputs']['text']='' #反向提示词 - cfui = ComfyUIApi(server_address="127.0.0.1:8188") # 根据自己comfyUI地址修改 - images = cfui.get_images(data2['prompt']) - return {'res': images[0]['image'], 'res_type': 'image', 'filename': images[0]['filename']} - - -@register_tool -def get_news() -> str: - ''' - 获取最新新闻 - ''' - news = News() - return news.get_important_news() - - -@register_tool -def get_time() -> str: - ''' - 获取当前日期,时间,农历日期,星期几 - ''' - time = datetime.now() - date2 = ZhDate.from_datetime(time) - week_list = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] - - return '{} {} {}'.format(time.strftime("%Y年%m月%d日 %H:%M:%S"), week_list[time.weekday()], '农历:' + date2.chinese()) - - -if __name__ == "__main__": - print(dispatch_tool("get_weather", {"city_name": "beijing"})) - print(get_tools()) diff --git a/base/func_bard.py b/base/func_bard.py deleted file mode 100644 index da3bee8..0000000 --- a/base/func_bard.py +++ /dev/null @@ -1,44 +0,0 @@ -#! /usr/bin/env python3 -# -*- coding: utf-8 -*- - -import os -import google.generativeai as genai - - -class BardAssistant: - def __init__(self, conf: dict) -> None: - self._api_key = conf["api_key"] - self._model_name = conf["model_name"] - self._prompt = conf['prompt'] - self._proxy = conf['proxy'] - - genai.configure(api_key=self._api_key) - self._bard = genai.GenerativeModel(self._model_name) - - def __repr__(self): - return 'BardAssistant' - - @staticmethod - def value_check(conf: dict) -> bool: - if conf: - if conf.get("api_key") and conf.get("model_name") and conf.get("prompt"): - return True - return False - - def get_answer(self, msg: str, sender: str = None) -> str: - response = self._bard.generate_content([{'role': 'user', 'parts': [msg]}]) - return response.text - - -if __name__ == "__main__": - from configuration import Config - config = Config().BardAssistant - if not config: - exit(0) - - bard_assistant = BardAssistant(config) - if bard_assistant._proxy: - os.environ['HTTP_PROXY'] = bard_assistant._proxy - os.environ['HTTPS_PROXY'] = bard_assistant._proxy - rsp = bard_assistant.get_answer(bard_assistant._prompt) - print(rsp) diff --git a/base/func_chatglm.py b/base/func_chatglm.py deleted file mode 100644 index 7db2bdf..0000000 --- a/base/func_chatglm.py +++ /dev/null @@ -1,195 +0,0 @@ -#! /usr/bin/env python3 -# -*- coding: utf-8 -*- - -import json -import os -import random -from datetime import datetime -from typing import Optional -import httpx -from openai import OpenAI -from base.chatglm.code_kernel import CodeKernel, execute -from base.chatglm.tool_registry import dispatch_tool, extract_code, get_tools -from wcferry import Wcf - -functions = get_tools() - - -class ChatGLM: - - def __init__(self, config={}, wcf: Optional[Wcf] = None, max_retry=5) -> None: - key = config.get("key", 'empty') - api = config.get("api") - proxy = config.get("proxy") - if proxy: - self.client = OpenAI(api_key=key, base_url=api, http_client=httpx.Client(proxy=proxy)) - else: - self.client = OpenAI(api_key=key, base_url=api) - self.conversation_list = {} - self.chat_type = {} - self.max_retry = max_retry - self.wcf = wcf - self.filePath = config["file_path"] - self.kernel = CodeKernel() - self.system_content_msg = {"chat": [{"role": "system", "content": config["prompt"]}], - "tool": [{"role": "system", - "content": "Answer the following questions as best as you can. You have access to the following tools:"}], - "code": [{"role": "system", - "content": "你是一位智能AI助手,你叫ChatGLM,你连接着一台电脑,但请注意不能联网。在使用Python解决任务时,你可以运行代码并得到结果,如果运行结果有错误,你需要尽可能对代码进行改进。你可以处理用户上传到电脑上的文件,文件默认存储路径是{}。".format( - self.filePath)}]} - - def __repr__(self): - return 'ChatGLM' - - @staticmethod - def value_check(conf: dict) -> bool: - if conf: - if conf.get("api") and conf.get("prompt") and conf.get("file_path"): - return True - return False - - def get_answer(self, question: str, wxid: str) -> str: - # wxid或者roomid,个人时为微信id,群消息时为群id - if '#帮助' == question: - return '本助手有三种模式,#聊天模式 = #1 ,#工具模式 = #2 ,#代码模式 = #3 , #清除模式会话 = #4 , #清除全部会话 = #5 可用发送#对应模式 或者 #编号 进行切换' - elif '#聊天模式' == question or '#1' == question: - self.chat_type[wxid] = 'chat' - return '已切换#聊天模式' - elif '#工具模式' == question or '#2' == question: - self.chat_type[wxid] = 'tool' - return '已切换#工具模式 \n工具有:查看天气,日期,新闻,comfyUI文生图。例如:\n帮我生成一张小鸟的图片,提示词必须是英文' - elif '#代码模式' == question or '#3' == question: - self.chat_type[wxid] = 'code' - return '已切换#代码模式 \n代码模式可以用于写python代码,例如:\n用python画一个爱心' - elif '#清除模式会话' == question or '#4' == question: - self.conversation_list[wxid][self.chat_type[wxid] - ] = self.system_content_msg[self.chat_type[wxid]] - return '已清除' - elif '#清除全部会话' == question or '#5' == question: - self.conversation_list[wxid] = self.system_content_msg - return '已清除' - - self.updateMessage(wxid, question, "user") - - try: - params = dict(model="chatglm3", temperature=1.0, - messages=self.conversation_list[wxid][self.chat_type[wxid]], stream=False) - if 'tool' == self.chat_type[wxid]: - params["tools"] = [dict(type='function', function=d) for d in functions.values()] - response = self.client.chat.completions.create(**params) - for _ in range(self.max_retry): - if response.choices[0].message.get("function_call"): - function_call = response.choices[0].message.function_call - print( - f"Function Call Response: {function_call.to_dict_recursive()}") - - function_args = json.loads(function_call.arguments) - observation = dispatch_tool( - function_call.name, function_args) - if isinstance(observation, dict): - res_type = observation['res_type'] if 'res_type' in observation else 'text' - res = observation['res'] if 'res_type' in observation else str( - observation) - if res_type == 'image': - filename = observation['filename'] - filePath = os.path.join(self.filePath, filename) - res.save(filePath) - self.wcf and self.wcf.send_image(filePath, wxid) - tool_response = '[Image]' if res_type == 'image' else res - else: - tool_response = observation if isinstance( - observation, str) else str(observation) - print(f"Tool Call Response: {tool_response}") - - params["messages"].append(response.choices[0].message) - params["messages"].append( - { - "role": "function", - "name": function_call.name, - "content": tool_response, # 调用函数返回结果 - } - ) - self.updateMessage(wxid, tool_response, "function") - response = self.client.chat.completions.create(**params) - elif response.choices[0].message.content.find('interpreter') != -1: - output_text = response.choices[0].message.content - code = extract_code(output_text) - self.wcf and self.wcf.send_text('代码如下:\n' + code, wxid) - self.wcf and self.wcf.send_text('执行代码...', wxid) - try: - res_type, res = execute(code, self.kernel) - except Exception as e: - rsp = f'代码执行错误: {e}' - break - if res_type == 'image': - filename = '{}.png'.format(''.join(random.sample( - 'abcdefghijklmnopqrstuvwxyz1234567890', 8))) - filePath = os.path.join(self.filePath, filename) - res.save(filePath) - self.wcf and self.wcf.send_image(filePath, wxid) - else: - self.wcf and self.wcf.send_text("执行结果:\n" + res, wxid) - tool_response = '[Image]' if res_type == 'image' else res - print("Received:", res_type, res) - params["messages"].append(response.choices[0].message) - params["messages"].append( - { - "role": "function", - "name": "interpreter", - "content": tool_response, # 调用函数返回结果 - } - ) - self.updateMessage(wxid, tool_response, "function") - response = self.client.chat.completions.create(**params) - else: - rsp = response.choices[0].message.content - break - - self.updateMessage(wxid, rsp, "assistant") - except Exception as e0: - rsp = "发生未知错误:" + str(e0) - - return rsp - - def updateMessage(self, wxid: str, question: str, role: str) -> None: - now_time = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) - - # 初始化聊天记录,组装系统信息 - if wxid not in self.conversation_list.keys(): - self.conversation_list[wxid] = self.system_content_msg - if wxid not in self.chat_type.keys(): - self.chat_type[wxid] = 'chat' - - # 当前问题 - content_question_ = {"role": role, "content": question} - self.conversation_list[wxid][self.chat_type[wxid]].append( - content_question_) - - # 只存储10条记录,超过滚动清除 - i = len(self.conversation_list[wxid][self.chat_type[wxid]]) - if i > 10: - print("滚动清除微信记录:" + wxid) - # 删除多余的记录,倒着删,且跳过第一个的系统消息 - del self.conversation_list[wxid][self.chat_type[wxid]][1] - - -if __name__ == "__main__": - from configuration import Config - - config = Config().CHATGLM - if not config: - exit(0) - - chat = ChatGLM(config) - - while True: - q = input(">>> ") - try: - time_start = datetime.now() # 记录开始时间 - print(chat.get_answer(q, "wxid")) - time_end = datetime.now() # 记录结束时间 - - # 计算的时间差为程序的执行时间,单位为秒/s - print(f"{round((time_end - time_start).total_seconds(), 2)}s") - except Exception as e: - print(e) diff --git a/config.yaml b/config.yaml index 382520c..4f6efc6 100644 --- a/config.yaml +++ b/config.yaml @@ -139,4 +139,13 @@ redis_config: host: "192.168.2.40" port: 6379 db: 0 - decode_responses: true \ No newline at end of file + decode_responses: true + + +#gewechat 配置 + +gewechat: + base_url: "http://192.168.2.240:2531/v2/api" + gewechat_token: "cb43f52db27e4a56bb6ec7da54373582" + app_id: "wx_3BC6eSHGE5xEm_hH3__7c" + callback_url : "http://192.168.2.192:8999/gewechat/callback" diff --git a/configuration.py b/configuration.py index 959b193..df07977 100644 --- a/configuration.py +++ b/configuration.py @@ -44,3 +44,42 @@ class Config(object): # DB config self.mariadb = yconfig.get("db_config", {}) self.redis = yconfig.get("redis_config", {}) + + #gewechat config + gewechat_config = yconfig['gewechat'] + self.BASE_URL = gewechat_config.get("base_url", "") + self.GEWECHAT_TOKEN = gewechat_config.get("gewechat_token", "") + self.APP_ID = gewechat_config.get("app_id", "") + self.CALLBACK_URL = gewechat_config.get("callback_url", "") + + def update_config(self, section, key, value): + """更新配置文件中指定部分的键值 + + Args: + section: 配置部分名称,如 'gewechat' + key: 键名,如 'app_id' + value: 要设置的值 + """ + import yaml + import os + + config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.yaml') + + # 读取当前配置 + with open(config_path, 'r', encoding='utf-8') as f: + config_data = yaml.safe_load(f) + + # 更新配置 + if section in config_data: + config_data[section][key] = value + + # 写回配置文件 + with open(config_path, 'w', encoding='utf-8') as f: + yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True) + + # 更新当前实例的属性 + if hasattr(self, key.upper()): + setattr(self, key.upper(), value) + + return True + return False \ No newline at end of file diff --git a/db/contacts_db.py b/db/contacts_db.py new file mode 100644 index 0000000..6b2c798 --- /dev/null +++ b/db/contacts_db.py @@ -0,0 +1,458 @@ +# -*- coding: utf-8 -*- +""" +微信联系人数据库操作类 +用于管理微信联系人信息的存储和查询 +""" + +import logging +import json +from typing import List, Dict, Optional, Union, Any + +from db.connection import DBConnectionManager + +logger = logging.getLogger(__name__) + +class ContactsDBOperator: + """微信联系人数据库操作类""" + + def __init__(self, db_manager: Optional[DBConnectionManager] = None): + """初始化联系人数据库操作类 + + Args: + db_manager: 数据库连接管理器,如果为None则自动获取单例 + """ + self.db_manager = db_manager or DBConnectionManager.get_instance() + self._ensure_table_exists() + + def _ensure_table_exists(self): + """确保联系人表存在""" + try: + # 创建联系人表 + sql = """ + CREATE TABLE IF NOT EXISTS t_wechat_contacts ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_name VARCHAR(64) NOT NULL COMMENT '微信ID', + nick_name VARCHAR(128) COMMENT '昵称', + py_initial VARCHAR(128) COMMENT '拼音首字母', + quan_pin VARCHAR(256) COMMENT '全拼', + sex TINYINT COMMENT '性别:1男,2女,0未知', + remark VARCHAR(128) COMMENT '备注', + remark_py_initial VARCHAR(128) COMMENT '备注拼音首字母', + remark_quan_pin VARCHAR(256) COMMENT '备注全拼', + signature TEXT COMMENT '个性签名', + alias VARCHAR(128) COMMENT '微信号', + sns_bg_img TEXT COMMENT '朋友圈背景图', + country VARCHAR(64) COMMENT '国家', + province VARCHAR(64) COMMENT '省份', + city VARCHAR(64) COMMENT '城市', + big_head_img_url TEXT COMMENT '大头像URL', + small_head_img_url TEXT COMMENT '小头像URL', + description TEXT COMMENT '描述', + card_img_url TEXT COMMENT '名片图片URL', + label_list TEXT COMMENT '标签列表', + phone_num_list TEXT COMMENT '电话号码列表', + type ENUM('friends', 'chatrooms', 'ghs') NOT NULL COMMENT '联系人类型:好友、群聊、公众号', + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + UNIQUE KEY `idx_user_name` (`user_name`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信联系人信息表'; + """ + conn = self.db_manager.get_connection() + cursor = conn.cursor() + cursor.execute(sql) + + # 创建群成员表 - 增加了更多字段以支持详细信息 + sql_chatroom_member = """ + CREATE TABLE IF NOT EXISTS t_chatroom_member ( + id INT AUTO_INCREMENT PRIMARY KEY, + chatroom_id VARCHAR(64) NOT NULL COMMENT '群聊ID', + wxid VARCHAR(64) NOT NULL COMMENT '成员微信ID', + nick_name VARCHAR(128) COMMENT '成员昵称', + display_name VARCHAR(128) COMMENT '群内显示名称', + inviter_user_name VARCHAR(64) COMMENT '邀请人微信ID', + member_flag INT COMMENT '成员标志,2049表示管理员', + big_head_img_url TEXT COMMENT '大头像URL', + small_head_img_url TEXT COMMENT '小头像URL', + is_owner TINYINT(1) DEFAULT 0 COMMENT '是否群主:0否,1是', + is_admin TINYINT(1) DEFAULT 0 COMMENT '是否管理员:0否,1是', + sex TINYINT COMMENT '性别:1男,2女,0未知', + signature TEXT COMMENT '个性签名', + alias VARCHAR(128) COMMENT '微信号', + country VARCHAR(64) COMMENT '国家', + province VARCHAR(64) COMMENT '省份', + city VARCHAR(64) COMMENT '城市', + label_list TEXT COMMENT '标签列表', + phone_num_list TEXT COMMENT '电话号码列表', + py_initial VARCHAR(128) COMMENT '拼音首字母', + quan_pin VARCHAR(256) COMMENT '全拼', + remark_py_initial VARCHAR(128) COMMENT '备注拼音首字母', + remark_quan_pin VARCHAR(256) COMMENT '备注全拼', + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + UNIQUE KEY `idx_chatroom_member` (`chatroom_id`, `wxid`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信群成员信息表'; + """ + cursor.execute(sql_chatroom_member) + + conn.commit() + logger.info("成功创建或确认微信联系人表和群成员表存在") + except Exception as e: + logger.error(f"创建微信联系人表或群成员表失败: {e}") + raise + finally: + if 'cursor' in locals(): + cursor.close() + if 'conn' in locals(): + self.db_manager.release_connection(conn) + + def save_contacts(self, contacts_data: List[Dict], contact_type: str) -> bool: + """保存联系人信息到数据库 + + Args: + contacts_data: 联系人数据列表 + contact_type: 联系人类型,可选值:'friends', 'chatrooms', 'ghs' + + Returns: + bool: 是否成功保存 + """ + if not contacts_data: + logger.warning(f"没有{contact_type}类型的联系人数据需要保存") + return True + + try: + conn = self.db_manager.get_connection() + cursor = conn.cursor() + + for contact in contacts_data: + # 将驼峰命名转换为下划线命名 + data = { + 'user_name': contact.get('userName', ''), + 'nick_name': contact.get('nickName', ''), + 'py_initial': contact.get('pyInitial', ''), + 'quan_pin': contact.get('quanPin', ''), + 'sex': contact.get('sex', 0), + 'remark': contact.get('remark', ''), + 'remark_py_initial': contact.get('remarkPyInitial', ''), + 'remark_quan_pin': contact.get('remarkQuanPin', ''), + 'signature': contact.get('signature', ''), + 'alias': contact.get('alias', ''), + 'sns_bg_img': contact.get('snsBgImg', ''), + 'country': contact.get('country', ''), + 'province': contact.get('province', ''), + 'city': contact.get('city', ''), + 'big_head_img_url': contact.get('bigHeadImgUrl', ''), + 'small_head_img_url': contact.get('smallHeadImgUrl', ''), + 'description': contact.get('description', ''), + 'card_img_url': contact.get('cardImgUrl', ''), + 'label_list': contact.get('labelList', ''), + 'phone_num_list': json.dumps(contact.get('phoneNumList', [])) if contact.get('phoneNumList') else '', + 'type': contact_type + } + + # 构建SQL语句 + fields = ', '.join(data.keys()) + placeholders = ', '.join(['%s'] * len(data)) + values = tuple(data.values()) + + # 使用INSERT ... ON DUPLICATE KEY UPDATE语法 + update_clause = ', '.join([f"{k}=VALUES({k})" for k in data.keys() if k != 'user_name']) + + sql = f""" + INSERT INTO t_wechat_contacts ({fields}) + VALUES ({placeholders}) + ON DUPLICATE KEY UPDATE {update_clause} + """ + + cursor.execute(sql, values) + + conn.commit() + logger.info(f"成功保存{len(contacts_data)}个{contact_type}类型的联系人") + return True + + except Exception as e: + logger.error(f"保存{contact_type}类型的联系人失败: {e}") + if 'conn' in locals(): + conn.rollback() + return False + finally: + if 'cursor' in locals(): + cursor.close() + if 'conn' in locals(): + self.db_manager.release_connection(conn) + + def save_simple_contacts(self, contact_list: List[str], contact_type: str) -> bool: + """保存简单联系人列表(只有user_name)到数据库 + + Args: + contact_list: 联系人ID列表 + contact_type: 联系人类型,可选值:'friends', 'chatrooms', 'ghs' + + Returns: + bool: 是否成功保存 + """ + if not contact_list: + logger.warning(f"没有{contact_type}类型的联系人数据需要保存") + return True + + try: + conn = self.db_manager.get_connection() + cursor = conn.cursor() + + for user_name in contact_list: + # 构建SQL语句 + sql = """ + INSERT INTO t_wechat_contacts (user_name, type) + VALUES (%s, %s) + ON DUPLICATE KEY UPDATE type = VALUES(type), update_time = CURRENT_TIMESTAMP + """ + + cursor.execute(sql, (user_name, contact_type)) + + conn.commit() + logger.info(f"成功保存{len(contact_list)}个{contact_type}类型的简单联系人") + return True + + except Exception as e: + logger.error(f"保存{contact_type}类型的简单联系人失败: {e}") + if 'conn' in locals(): + conn.rollback() + return False + finally: + if 'cursor' in locals(): + cursor.close() + if 'conn' in locals(): + self.db_manager.release_connection(conn) + + def get_contacts_by_type(self, contact_type: str) -> List[Dict]: + """根据类型获取联系人列表 + + Args: + contact_type: 联系人类型,可选值:'friends', 'chatrooms', 'ghs' + + Returns: + List[Dict]: 联系人列表 + """ + try: + conn = self.db_manager.get_connection() + cursor = conn.cursor(dictionary=True) + + sql = """ + SELECT * FROM t_wechat_contacts + WHERE type = %s + ORDER BY nick_name + """ + + cursor.execute(sql, (contact_type,)) + results = cursor.fetchall() + + return results + except Exception as e: + logger.error(f"获取{contact_type}类型的联系人失败: {e}") + return [] + finally: + if 'cursor' in locals(): + cursor.close() + if 'conn' in locals(): + self.db_manager.release_connection(conn) + + def get_contact_by_user_name(self, user_name: str) -> Optional[Dict]: + """根据user_name获取联系人信息 + + Args: + user_name: 联系人ID + + Returns: + Optional[Dict]: 联系人信息,如果不存在则返回None + """ + try: + conn = self.db_manager.get_connection() + cursor = conn.cursor(dictionary=True) + + sql = """ + SELECT * FROM t_wechat_contacts + WHERE user_name = %s + LIMIT 1 + """ + + cursor.execute(sql, (user_name,)) + result = cursor.fetchone() + + return result + except Exception as e: + logger.error(f"获取联系人{user_name}失败: {e}") + return None + finally: + if 'cursor' in locals(): + cursor.close() + if 'conn' in locals(): + self.db_manager.release_connection(conn) + + def get_display_name(self, user_name: str) -> str: + """获取联系人的显示名称(优先使用备注,其次是昵称,最后是微信ID) + + Args: + user_name: 联系人ID + + Returns: + str: 显示名称 + """ + contact = self.get_contact_by_user_name(user_name) + if not contact: + return user_name + + return contact.get('remark') or contact.get('nick_name') or user_name + + def get_all_contacts_name_map(self) -> Dict[str, str]: + """获取所有联系人的ID到显示名称的映射 + + Returns: + Dict[str, str]: 联系人ID到显示名称的映射 + """ + try: + conn = self.db_manager.get_connection() + cursor = conn.cursor() + + sql = """ + SELECT user_name, remark, nick_name FROM t_wechat_contacts + """ + + cursor.execute(sql) + results = cursor.fetchall() + + name_map = {} + for user_name, remark, nick_name in results: + display_name = remark or nick_name or user_name + name_map[user_name] = display_name + + return name_map + except Exception as e: + logger.error(f"获取所有联系人名称映射失败: {e}") + return {} + finally: + if 'cursor' in locals(): + cursor.close() + if 'conn' in locals(): + self.db_manager.release_connection(conn) + + def save_chatroom_member_detail(self, chatroom_id: str, member_details: List[Dict]) -> bool: + """保存群成员详细信息到数据库 + + Args: + chatroom_id: 群聊ID + member_details: 群成员详细信息列表 + + Returns: + bool: 是否成功保存 + """ + if not member_details or not chatroom_id: + logger.warning(f"没有群聊{chatroom_id}的成员详细信息需要保存") + return False + + try: + conn = self.db_manager.get_connection() + cursor = conn.cursor() + + # 获取现有的群成员信息,以便更新而不是替换 + existing_members_sql = """ + SELECT wxid, is_owner, is_admin FROM t_chatroom_member + WHERE chatroom_id = %s + """ + cursor.execute(existing_members_sql, (chatroom_id,)) + existing_members = {row[0]: (row[1], row[2]) for row in cursor.fetchall()} + + for member in member_details: + wxid = member.get('userName', '') + if not wxid: + continue + + # 保留现有的群主和管理员标识 + is_owner, is_admin = 0, 0 + if wxid in existing_members: + is_owner, is_admin = existing_members[wxid] + + # 处理电话号码列表 + phone_num_list = member.get('phoneNumList', []) + if phone_num_list: + phone_num_str = json.dumps(phone_num_list) + else: + phone_num_str = '' + + # 构建数据 + data = { + 'chatroom_id': chatroom_id, + 'wxid': wxid, + 'nick_name': member.get('nickName', ''), + 'display_name': member.get('remark', ''), # 使用备注作为群内显示名称 + 'inviter_user_name': member.get('inviterUserName', ''), + 'member_flag': member.get('memberFlag', 0), + 'big_head_img_url': member.get('bigHeadImgUrl', ''), + 'small_head_img_url': member.get('smallHeadImgUrl', ''), + 'is_owner': is_owner, + 'is_admin': is_admin, + # 额外的详细信息字段 + 'sex': member.get('sex', 0), + 'signature': member.get('signature', ''), + 'alias': member.get('alias', ''), + 'country': member.get('country', ''), + 'province': member.get('province', ''), + 'city': member.get('city', ''), + 'label_list': member.get('labelList', ''), + 'phone_num_list': phone_num_str, + 'py_initial': member.get('pyInitial', ''), + 'quan_pin': member.get('quanPin', ''), + 'remark_py_initial': member.get('remarkPyInitial', ''), + 'remark_quan_pin': member.get('remarkQuanPin', '') + } + + # 构建SQL语句 - 使用REPLACE INTO确保更新现有记录 + fields = ', '.join(data.keys()) + placeholders = ', '.join(['%s'] * len(data)) + values = tuple(data.values()) + + sql = f""" + REPLACE INTO t_chatroom_member ({fields}) + VALUES ({placeholders}) + """ + + cursor.execute(sql, values) + + conn.commit() + logger.info(f"成功保存群聊{chatroom_id}的{len(member_details)}个成员详细信息") + return True + + except Exception as e: + logger.error(f"保存群聊{chatroom_id}的成员详细信息失败: {e}") + if 'conn' in locals(): + conn.rollback() + return False + finally: + if 'cursor' in locals(): + cursor.close() + if 'conn' in locals(): + self.db_manager.release_connection(conn) + + def process_chatroom_member_detail_response(self, chatroom_id: str, response: Dict) -> bool: + """处理获取群成员详情的API响应 + + Args: + chatroom_id: 群聊ID + response: API响应数据 + + Returns: + bool: 是否成功处理 + """ + try: + if response.get('ret') != 200: + logger.error(f"获取群聊{chatroom_id}成员详情失败: {response.get('msg')}") + return False + + data = response.get('data', []) + if not data: + logger.warning(f"群聊{chatroom_id}成员详情数据为空") + return False + + return self.save_chatroom_member_detail(chatroom_id, data) + + except Exception as e: + logger.error(f"处理群聊{chatroom_id}成员详情数据失败: {e}") + return False \ No newline at end of file diff --git a/db/message_storage.py b/db/message_storage.py index 1b4825b..3c4143a 100644 --- a/db/message_storage.py +++ b/db/message_storage.py @@ -3,10 +3,9 @@ from datetime import datetime from typing import Dict, List, Optional -from wcferry import WxMsg - from db.base import BaseDBOperator from db.connection import DBConnectionManager +from gewechat.call_back_message.message import WxMessage class MessageStorageDB(BaseDBOperator): @@ -15,14 +14,15 @@ class MessageStorageDB(BaseDBOperator): def __init__(self, db_manager: DBConnectionManager): super().__init__(db_manager) - def archive_message(self, msg: WxMsg) -> bool: + def archive_message(self, msg: WxMessage) -> bool: """存档消息""" now_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") sql = """ INSERT INTO messages (group_id, timestamp, sender, content, message_type, attachment_url, message_id, message_xml, message_thumb) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) """ - params = (msg.roomid, now_time, msg.sender, msg.content, msg.type, msg.extra, msg.id, msg.xml, msg.thumb) + params = ( + msg.roomid, now_time, msg.sender, msg.content, msg.msg_type, msg.content, msg.msg_id, msg.msg_source, "") result = self.execute_update(sql, params) return result @@ -51,7 +51,6 @@ class MessageStorageDB(BaseDBOperator): """ return self.execute_query(sql, (date,)) or [] - def get_speech_ranking(self, date: str, group_id: str, limit: int = 20) -> List[Dict]: """获取指定日期和群组的发言排名""" sql = """ @@ -87,7 +86,6 @@ class MessageStorageDB(BaseDBOperator): params = (group_id, wx_id, date, count) return self.execute_update(sql, params) - def get_message_trend(self, group_id: str, days: int = 7) -> List[Dict]: """获取指定群组的消息趋势数据 @@ -111,7 +109,7 @@ class MessageStorageDB(BaseDBOperator): return self.execute_query(sql, (group_id, days)) or [] def get_messages_by_filter(self, group_id=None, start_date=None, end_date=None, - search_text=None, page=1, page_size=20) -> Dict: + search_text=None, page=1, page_size=20) -> Dict: """按条件筛选消息并支持分页和模糊搜索 Args: @@ -200,11 +198,11 @@ class MessageStorageDB(BaseDBOperator): WHERE message_id = %s """ params = (image_path, message_id) - + # 执行更新操作 result = self.execute_update(sql, params) return result except Exception as e: # 使用已有的日志记录方式 print(f"更新消息图片路径出错: {e}") - return False \ No newline at end of file + return False diff --git a/gewechat/__init__.py b/gewechat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gewechat/api/callback.py b/gewechat/api/callback.py new file mode 100644 index 0000000..8da4a00 --- /dev/null +++ b/gewechat/api/callback.py @@ -0,0 +1,122 @@ +from fastapi import APIRouter, Request +from gewechat.call_back_message.message import WxMessage, MessageType, AppMessageType +import logging + +from robot import Robot + +router = APIRouter() +logger = logging.getLogger(__name__) + +# 存储Robot实例的字典,以appid为键 +robot_instances = {} + + +def register_robot(appid, robot_instance): + """注册Robot实例""" + robot_instances[appid] = robot_instance + logger.info(f"已注册appid为{appid}的Robot实例") + + +@router.post("/gewechat/callback") +async def callback(request: Request): + """接收微信消息回调""" + try: + # 获取原始JSON数据 + json_data = await request.json() + + # 创建消息对象 + msg = WxMessage.from_json(json_data) + + # 根据消息类型处理 + if msg.type_name == "AddMsg": + await handle_add_message(msg) + elif msg.type_name == "ModContacts": + await handle_mod_contacts(msg) + elif msg.type_name == "DelContacts": + await handle_del_contacts(msg) + elif msg.type_name == "Offline": + await handle_offline(msg) + + return {"code": 0, "message": "success"} + + except Exception as e: + logger.error(f"处理回调消息失败: {str(e)}", exc_info=True) + return {"code": -1, "message": f"处理失败: {str(e)}"} + + +async def handle_add_message(msg: WxMessage): + """处理新消息""" + try: + # 获取对应的Robot实例 + robot: Robot = robot_instances.get(msg.appid) + if robot: + # 调用Robot的onMsg方法处理消息 + robot.onMsg(msg) + else: + logger.warning(f"未找到appid为{msg.appid}的Robot实例") + + except Exception as e: + logger.error(f"处理新消息失败: {str(e)}", exc_info=True) + raise + + +async def handle_mod_contacts(msg: WxMessage): + """处理联系人变更""" + logger.info(f"联系人信息变更: {msg.raw_data}") + # 获取对应的Robot实例并刷新联系人 + robot = robot_instances.get(msg.appid) + if robot: + robot.refresh_contacts() + + +async def handle_del_contacts(msg: WxMessage): + """处理联系人删除""" + logger.info(f"联系人被删除: {msg.raw_data}") + # 获取对应的Robot实例并刷新联系人 + robot = robot_instances.get(msg.appid) + if robot: + robot.refresh_contacts() + + +async def handle_offline(msg: WxMessage): + """处理离线通知""" + logger.info(f"账号离线: {msg.wxid}") + # 可以在这里处理账号离线逻辑 + + +async def handle_text_message(msg: WxMessage): + """处理文本消息""" + logger.info(f"收到文本消息: {msg.content.raw_content}") + # TODO: 实现文本消息处理逻辑 + + +async def handle_image_message(msg: WxMessage): + """处理图片消息""" + image_content = msg.get_image_content() + if image_content: + logger.info(f"收到图片消息: {image_content.url}") + # TODO: 实现图片消息处理逻辑 + + +async def handle_app_message(msg: WxMessage): + """处理应用消息""" + app_type = msg.get_app_message_type() + if app_type == AppMessageType.LINK: + logger.info("收到链接消息") + elif app_type == AppMessageType.FILE: + logger.info("收到文件消息") + elif app_type == AppMessageType.MINIPROGRAM: + logger.info("收到小程序消息") + # TODO: 实现应用消息处理逻辑 + + +async def handle_system_message(msg: WxMessage): + """处理系统消息""" + logger.info(f"收到系统消息: {msg.content.raw_content}") + # TODO: 实现系统消息处理逻辑 + + +async def handle_system_notify(msg: WxMessage): + """处理系统通知""" + logger.info(f"收到系统通知: {msg.content.raw_content}") + # TODO: 实现系统通知处理逻辑 diff --git a/gewechat/api/start_server.py b/gewechat/api/start_server.py new file mode 100644 index 0000000..8f0daaa --- /dev/null +++ b/gewechat/api/start_server.py @@ -0,0 +1,85 @@ +import threading +import logging +import socket +import uvicorn +from fastapi import FastAPI + +from gewechat.api.callback import router as callback_router + +# 配置日志 +logger = logging.getLogger(__name__) + +def is_port_in_use(port, host='0.0.0.0'): + """检查端口是否被占用""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.bind((host, port)) + return False + except socket.error: + return True + +def start_fastapi_server(host="0.0.0.0", port=8999): + """启动FastAPI服务器""" + # 检查端口是否被占用 + if is_port_in_use(port, host): + logger.warning(f"端口 {port} 已被占用,尝试使用其他端口") + # 尝试其他端口 + for test_port in range(9000, 9100): + if not is_port_in_use(test_port, host): + port = test_port + break + else: + logger.error("无法找到可用端口,服务器启动失败") + return False + + try: + app = FastAPI() + app.include_router(callback_router) + + # 添加健康检查路由 + @app.get("/health") + async def health_check(): + return {"status": "ok"} + + logger.info(f"正在启动FastAPI服务器,地址: http://{host}:{port}") + + # 使用线程启动uvicorn服务器 + server_thread = threading.Thread( + target=uvicorn.run, + args=(app,), + kwargs={"host": host, "port": port, "log_level": "info"}, + daemon=True + ) + server_thread.start() + logger.info(f"FastAPI 服务已在 http://{host}:{port} 启动") + logger.info(f"回调URL: http://{host}:{port}/gewechat/callback") + + # 返回启动的端口,以便调用者知道实际使用的端口 + return port + except Exception as e: + logger.error(f"启动FastAPI服务器失败: {e}", exc_info=True) + return False + +if __name__ == '__main__': + # 配置日志 + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # 启动服务器 + port = start_fastapi_server() + if port: + print(f"服务器启动成功,端口: {port}") + print(f"回调URL: http://localhost:{port}/gewechat/callback") + print(f"健康检查URL: http://localhost:{port}/health") + + # 保持主线程运行 + try: + import time + while True: + time.sleep(1) + except KeyboardInterrupt: + print("服务器已停止") + else: + print("服务器启动失败") \ No newline at end of file diff --git a/gewechat/call_back_message/message.py b/gewechat/call_back_message/message.py new file mode 100644 index 0000000..08c4cba --- /dev/null +++ b/gewechat/call_back_message/message.py @@ -0,0 +1,299 @@ +from dataclasses import dataclass +from typing import Optional, Dict, Any +from enum import Enum +import xml.etree.ElementTree as ET +import json + + +class MessageType(Enum): + """消息类型枚举""" + TEXT = 1 # 文本消息 + IMAGE = 3 # 图片消息 + VOICE = 34 # 语音消息 + VIDEO = 43 # 视频消息 + EMOJI = 47 # emoji表情 + LOCATION = 48 # 地理位置 + APP = 49 # 应用消息(链接、文件、小程序等) + SYSTEM = 10000 # 系统消息 + SYSTEM_NOTIFY = 10002 # 系统通知 + + +class AppMessageType(Enum): + """应用消息类型枚举""" + LINK = 5 # 链接消息 + FILE = 6 # 文件消息 + FILE_NOTICE = 74 # 文件上传通知 + MINIPROGRAM = 33 # 小程序消息 + QUOTE = 57 # 引用消息 + TRANSFER = 2000 # 转账消息 + RED_PACKET = 2001 # 红包消息 + CHANNELS = 51 # 视频号消息 + + +@dataclass +class MessageContent: + """消息内容""" + raw_content: str # 原始内容 + xml_content: Optional[ET.Element] = None # XML内容(如果有) + + def __post_init__(self): + """处理XML内容""" + if self.raw_content.startswith(' 'WxMessage': + """从JSON数据创建消息对象""" + data = json_data.get("Data", {}) + to_user = data.get("ToUserName", {}).get("string", "") + + return cls( + type_name=json_data.get("TypeName", ""), + appid=json_data.get("Appid", ""), + wxid=json_data.get("Wxid", ""), + msg_id=data.get("MsgId", 0), + sender=data.get("FromUserName", {}).get("string", ""), + to_user=to_user, + roomid=to_user if to_user.endswith("@chatroom") else "", # 设置room_id + msg_type=MessageType(data.get("MsgType", 0)), + content=MessageContent(data.get("Content", {}).get("string", "")), + create_time=data.get("CreateTime", 0), + push_content=data.get("PushContent"), + new_msg_id=data.get("NewMsgId", 0), + msg_seq=data.get("MsgSeq", 0), + msg_source=data.get("MsgSource", ""), + raw_data=json_data + ) + + def __str__(self) -> str: + """返回消息的字符串表示,用于打印和日志""" + # 获取消息类型的名称 + msg_type_name = self.msg_type.name if self.msg_type else "UNKNOWN" + + # 处理不同类型的消息内容 + content_str = "" + if self.msg_type == MessageType.TEXT: + # 文本消息直接显示内容 + content_str = self.content.raw_content + elif self.msg_type == MessageType.IMAGE: + # 图片消息显示图片信息 + img_content = self.get_image_content() + if img_content: + content_str = f"[图片] 大小: {img_content.length}字节, MD5: {img_content.md5}" + else: + content_str = "[图片]" + elif self.msg_type == MessageType.VOICE: + # 语音消息显示语音信息 + voice_content = self.get_voice_content() + if voice_content: + content_str = f"[语音] 长度: {voice_content.voice_length}ms" + else: + content_str = "[语音]" + elif self.msg_type == MessageType.VIDEO: + # 视频消息显示视频信息 + video_content = self.get_video_content() + if video_content: + content_str = f"[视频] 长度: {video_content.play_length}ms, 大小: {video_content.length}字节" + else: + content_str = "[视频]" + elif self.msg_type == MessageType.LOCATION: + # 位置消息显示位置信息 + location_content = self.get_location_content() + if location_content: + content_str = f"[位置] {location_content.label}" + else: + content_str = "[位置]" + elif self.msg_type == MessageType.APP: + # 应用消息显示应用类型 + app_type = self.get_app_message_type() + if app_type: + content_str = f"[应用消息] 类型: {app_type.name}" + else: + content_str = "[应用消息]" + elif self.msg_type == MessageType.EMOJI: + content_str = "[表情]" + elif self.msg_type == MessageType.SYSTEM: + content_str = f"[系统消息] {self.content.raw_content}" + elif self.msg_type == MessageType.SYSTEM_NOTIFY: + content_str = f"[系统通知] {self.content.raw_content}" + else: + # 其他类型消息 + content_str = f"[未知类型消息] {self.content.raw_content[:30]}..." + + # 限制内容长度,避免过长 + if len(content_str) > 100: + content_str = content_str[:97] + "..." + + # 构建基本信息 + from_info = f"发送者: {self.sender}" + to_info = f"接收者: {self.to_user}" + + # 如果是群消息,添加群信息 + group_info = "" + if self.from_group(): + group_info = f"群聊: {self.roomid}, " + + # 构建完整的消息字符串 + return (f"WxMessage[ID: {self.msg_id}, 类型: {msg_type_name}, " + f"{group_info}{from_info}, {to_info}, " + f"内容: {content_str}]") + + def __repr__(self) -> str: + """返回消息的详细表示,用于调试""" + return self.__str__() + + def from_self(self) -> bool: + """判断是否是自己发送的消息""" + return self.sender == self.wxid + + def from_group(self) -> bool: + return self.to_user.endswith("@chatroom") + + def get_app_message_type(self) -> Optional[AppMessageType]: + """获取应用消息类型""" + if self.msg_type != MessageType.APP or not self.content.xml_content: + return None + + try: + appmsg = self.content.xml_content.find('.//appmsg') + if appmsg is not None: + type_value = int(appmsg.find('type').text) + return AppMessageType(type_value) + except (AttributeError, ValueError): + pass + return None + + def get_image_content(self) -> Optional[ImageContent]: + """获取图片消息内容""" + if self.msg_type != MessageType.IMAGE or not self.content.xml_content: + return None + + try: + img = self.content.xml_content.find('img') + if img is not None: + return ImageContent( + aes_key=img.get('aeskey', ''), + url=img.get('cdnthumburl', ''), + length=int(img.get('length', 0)), + md5=img.get('md5', ''), + thumb_base64=self.raw_data.get("Data", {}).get("ImgBuf", {}).get("buffer") + ) + except (AttributeError, ValueError): + pass + return None + + def get_voice_content(self) -> Optional[VoiceContent]: + """获取语音消息内容""" + if self.msg_type != MessageType.VOICE or not self.content.xml_content: + return None + + try: + voice = self.content.xml_content.find('.//voicemsg') + if voice is not None: + return VoiceContent( + voice_length=int(voice.get('voicelength', 0)), + aes_key=voice.get('aeskey', ''), + url=voice.get('voiceurl', ''), + voice_base64=self.raw_data.get("Data", {}).get("ImgBuf", {}).get("buffer") + ) + except (AttributeError, ValueError): + pass + return None + + def get_video_content(self) -> Optional[VideoContent]: + """获取视频消息内容""" + if self.msg_type != MessageType.VIDEO or not self.content.xml_content: + return None + + try: + video = self.content.xml_content.find('.//videomsg') + if video is not None: + return VideoContent( + aes_key=video.get('aeskey', ''), + video_url=video.get('cdnvideourl', ''), + thumb_url=video.get('cdnthumburl', ''), + length=int(video.get('length', 0)), + play_length=int(video.get('playlength', 0)) + ) + except (AttributeError, ValueError): + pass + return None + + def get_location_content(self) -> Optional[LocationContent]: + """获取地理位置内容""" + if self.msg_type != MessageType.LOCATION or not self.content.xml_content: + return None + + try: + location = self.content.xml_content.find('location') + if location is not None: + return LocationContent( + x=float(location.get('x', 0)), + y=float(location.get('y', 0)), + label=location.get('label', ''), + poi_name=location.get('poiname') + ) + except (AttributeError, ValueError): + pass + return None diff --git a/gewechat/call_back_message/model.md b/gewechat/call_back_message/model.md new file mode 100644 index 0000000..2bf724a --- /dev/null +++ b/gewechat/call_back_message/model.md @@ -0,0 +1,1200 @@ +# 回调消息详解 + +### 回调消息常见问题 + +Q. **微信在线为什么没有消息推送?** +``` +当回调消息未能通过 HTTP POST/JSON 方式成功推送至接收方时,请考虑使用 Apifox 向接收地址发送一条测试消息。如果仍然未能接收到消息,请检查接收地址的可用性。反之,若能成功接收测试消息,请联系客服,我们将协助您进行进一步的问题排查。 +``` + +Q. **如何判断是否是自己发送的消息?** +``` +可通过消息发送人($.Data.FromUserName.string)与所属微信($.Wxid)是否一致进行判断。 +``` + +Q. **为什么同一条消息会重复回调?** +``` +因服务重启、同步历史消息、失败重试等原因,同一条消息可能会重复推送,接收方需根据$.Appid+$.Data.NewMsgId字段做消息排重,以防消息重复处理。 +``` + +--- + +#### 文本消息 +```json + { + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356095, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 1, 消息类型 1是文本消息 + "Content": + { + "string": "123" # 消息内容 + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705043418, 消息发送时间 + "MsgSource": "\n\t\n\t\t1\n\t\n\tv1_volHXhv4\n\t\n\t\t\n\t\n\n", + "PushContent": "朝夕。 : 123", 消息通知内容 + "NewMsgId": 7773749793478223190, 消息ID + "MsgSeq": 640356095 + } + } +``` + + +#### 图片消息 +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356099, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 3, 消息类型 3是图片消息 + "Content": + { + "string": "\n\n\t\n\t\n\t\n\n" 图片的cdn信息,可用此字段做转发图片 + }, + "Status": 3, + "ImgStatus": 2, + "ImgBuf": + { + "iLen": 2146, + "buffer": "/9j/4AAQSkZJRgABAQAASABIAAD/4QBM..." # 缩略图的base64 + }, + "CreateTime": 1705043678, 消息发送时间 + "MsgSource": "\n\t\n\t\t2\n\t\n\t\n\t\t5b04ea0181f86c7f3d126e9a7fe5038b_\n\t\n\tv1_5WGxwSEj\n\t\n\t\t\n\t\n\n", + "PushContent": "朝夕。 : [图片]", 消息通知内容 + "NewMsgId": 6906713067183447582, 消息ID + "MsgSeq": 640356099 + } +} +``` + +#### 语音消息 +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356100, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 34, 消息类型,34是语音消息 + "Content": + { + "string": "" 语音消息的下载信息,可用于下载语音文件 + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 3600, + "buffer": "AiMhU0lMS19WMxMApzi9JA+qToPB..." 语音文件的base64,并非所有语音消息都有本字段 + }, + "CreateTime": 1705043782, 消息发送时间 + "MsgSource": "\n\tv1_j+rf/Jnp\n\t\n\t\t\n\t\n\n", + "PushContent": "朝夕。 : [语音]", 消息通知内容 + "NewMsgId": 1428830975092239121, 消息ID + "MsgSeq": 640356100 + } +} +``` + +#### 视频消息 +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356101, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 43, 消息类型,43是视频消息 + "Content": + { + "string": "\n\n\t\n\n" 视频消息的cdn信息,可用此字段做转发视频 + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705043879, 消息发送时间 + "MsgSource": "\n\t0\n\t\n\t\tce3ebc6d2893c7a2669ac5d2eaa4aadf_\n\t\n\tv1_kk/psF9W\n\t\n\t\t\n\t\n\n", + "PushContent": "朝夕。 : [视频]", 消息通知内容 + "NewMsgId": 6628526085342711793, 消息ID + "MsgSeq": 640356101 + } +} +``` + +#### emoji表情 +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356102, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 47, 消息类型,47是emoji消息 + "Content": + { + "string": " " 可解析xml中的md5用与发送emoji消息 + }, + "Status": 3, + "ImgStatus": 2, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705043947, 消息发送时间 + "MsgSource": "\n\tv1_vy/xC7WS\n\t\n\t\t\n\t\n\n", + "PushContent": "朝夕。 : [动画表情]", 消息通知内容 + "NewMsgId": 6674256223577965652, 消息ID + "MsgSeq": 640356102 + } +} +``` + +#### 公众号链接 +- 判断链接消息的逻辑:\$.Data.MsgType=49 并且 解析\$.Data.Content.string中的xml msg.appmsg.type=5,按此逻辑会匹配到两种消息,链接消息及邀请进群的通知,可依据xml msg.appmsg.title做区分 +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356105, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 49, + "Content": + { + "string": "\n\n\t\n\t\t尔滨,又有好消息!\n\t\t\n\t\t\n\t\t5\n\t\t0\n\t\t0\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t0\n\t\thttp://mp.weixin.qq.com/s?__biz=MzA4NDI3NjcyNA==&mid=2650011300&idx=1&sn=52739c3d39c030394da972e3d83efc98&chksm=86ed931f730a3e19a5edc840896d9bf1ad1f8b60cdccafea6a9e7a38a0a33f261877d334622b&scene=0&xtrack=1#rd\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\t0\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t3057020100044b304902010002048399cc8402032f7e350204a810d83a020465a0e829042462343663343435612d333737392d346230612d616434622d6263383038633562643562340204051408030201000405004c53d900\n\t\t\tadd1b4bcf9cc50c6a8f14ff334bc3d5c\n\t\t\t83741\n\t\t\t1000\n\t\t\t426\n\t\t\t37889a1e22c1e58ebd4e6589b999f63e\n\t\t\t\n\t\t\n\t\t\n\t\tgh_6651e07e4b2d\n\t\t新华社\n\t\thttps://mmbiz.qpic.cn/mmbiz_jpg/azXQmS1HA7mOP6LHArYqZ5ypK4iajvBdfhNxzyANcQ1eW7ec6yZVj7tv8Lt6tWftSNckDz3j4FqkP04TxARG8dQ/640?wxtype=jpeg&wxfrom=0\n\t\t\n\t\t\n\t\t\n\t\t\t0\n\t\t\n\t\n\twxid_phyyedw9xap22\n\t0\n\t\n\t\t1\n\t\t\n\t\n\t\n\n" 可用此字段做转发链接 + }, + "Status": 3, + "ImgStatus": 2, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705044033, 消息发送时间 + "MsgSource": "\n\t\n\t\t\n\t\n\t\n\t\t4\n\t\n\t\n\t\tba15c632e8fa89ed84bd027f09495591_\n\t\n\tv1_ptaEL1bv\n\n", + "PushContent": "朝夕。 : [链接]尔滨,又有好消息!", 消息通知内容 + "NewMsgId": 1623411326098221490, 消息ID + "MsgSeq": 640356105 + } +} +``` + +#### 文件消息(发送文件的通知) +- **注意**:收到本条消息仅代表对方在向你发送文件,并不可以用本条做转发及下载 +- 判断此类消息的逻辑:\$.Data.MsgType=49 并且 解析\$.Data.Content.string中的xml msg.appmsg.type=74 +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356106, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 49, + "Content": + { + "string": "\n \n <![CDATA[hhh.xlsx]]>\n 74\n 0\n \n 8939\n \n v1_paVQtd+CWGr2I3eOg71E6KBpQf0yY9RFQkqDPwT4yMnnbawqveao1vAE0qCOhWcIPkMGZavimUTDFcImr+SaManD8pKVQbBPTUvSmA6UsXgZWqQDOT00VLx7U/hoP3/CwveN2Lk56nxcef/XJiGKrOpAHKHcZvccaGk9/68wsBCOyanya/9xgdHTYxyQp4IadiSe\n 0\n \n \n \n \n wxid_phyyedw9xap22\n" + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705044119, 消息发送时间 + "MsgSource": "\n\tv1_WyLyIcy+\n\t\n\t\t\n\t\n\n", + "PushContent": "朝夕。 : [文件]hhh.xlsx", 消息通知内容 + "NewMsgId": 1789783684714859663, 消息ID + "MsgSeq": 640356106 + } +} +``` + +#### 文件消息(文件发送完成) +- **注意**:收到本条消息表示对方给你的文件发送完成,可用本条消息做转发及下载 +- 判断此类消息的逻辑:\$.Data.MsgType=49 并且 解析\$.Data.Content.string中的xml msg.appmsg.type=6 +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356107, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 49, + "Content": + { + "string": "\n\n\t\n\t\thhh.xlsx\n\t\t\n\t\t\n\t\t6\n\t\t0\n\t\t0\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t0\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\t8939\n\t\t\t@cdn_3057020100044b304902010002043904752002032f7e350204aa0dd83a020465a0e897042430373538386564322d353866642d343234342d386563652d6236353536306438623936610204011800050201000405004c56f900_3f28b0cbd65a86c3a980f3e22808c0fe_1\n\t\t\t\n\t\t\txlsx\n\t\t\t3057020100044b304902010002043904752002032f7e350204aa0dd83a020465a0e897042430373538386564322d353866642d343234342d386563652d6236353536306438623936610204011800050201000405004c56f900\n\t\t\t3f28b0cbd65a86c3a980f3e22808c0fe\n\t\t\t0\n\t\t\t1789783684714859663\n\t\t\tv1_paVQtd+CWGr2I3eOg71E6KBpQf0yY9RFQkqDPwT4yMnnbawqveao1vAE0qCOhWcIPkMGZavimUTDFcImr+SaManD8pKVQbBPTUvSmA6UsXgZWqQDOT00VLx7U/hoP3/CwveN2Lk56nxcef/XJiGKrOpAHKHcZvccaGk9/68wsBCOyanya/9xgdHTYxyQp4IadiSe\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t84c6737fe9549270c9b3ca4f6fc88f6f\n\t\t\n\t\n\twxid_phyyedw9xap22\n\t0\n\t\n\t\t1\n\t\t\n\t\n\t\n\n" + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705044119, 消息发送时间 + "MsgSource": "\n\t\n\t\t3\n\t\n\t\n\t\t896374a2b5979141804d509256c22f0b_\n\t\n\tv1_n7kZ01bp\n\t\n\t\t\n\t\n\n", + "PushContent": "朝夕。 : [文件]hhh.xlsx", 消息通知内容 + "NewMsgId": 3617029648443513152, 消息ID + "MsgSeq": 640356107 + } +} +``` + +#### 名片消息 +```json + { + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356108, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 42, 消息类型,42是名片消息 + "Content": + { + "string": "\n\n" 名片中微信号的基本信息,可用于添加好友 + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705044829, 消息发送时间 + "MsgSource": "\n\t0\n\t\n\t\t2\n\t\n\tv1_bawbB33Z\n\t\n\t\t\n\t\n\n", + "PushContent": "朝夕。 : [名片]Ashley", 消息通知内容 + "NewMsgId": 766322251431765776, 消息ID + "MsgSeq": 640356108 + } + } +``` + +#### 好友添加请求通知 +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356166, 消息ID + "FromUserName": + { + "string": "fmessage" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 37, 消息类型,37是好友添加请求通知 + "Content": + { + "string": "" 请求添加好友微信号的基本信息,可用于添加好友 + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045979, 消息发送时间 + "MsgSource": "\n\tv1_GOrHWRNL\n\t\n\t\t\n\t\n\n", + "NewMsgId": 1109510141823131559, 消息ID + "MsgSeq": 640356166 + } +} +``` + +#### 好友通过验证及好友资料变更的通知消息 +```json +{ + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", + "TypeName": "ModContacts", + "Data": + { + "UserName": + { + "string": "wxid_0xsqb3o0tsvz22" + }, + "NickName": + { + "string": "chaoxi。" + }, + "PyInitial": + { + "string": "CX" + }, + "QuanPin": + { + "string": "chaoxi" + }, + "Sex": 1, + "ImgBuf": + { + "iLen": 0 + }, + "BitMask": 4294967295, + "BitVal": 3, + "ImgFlag": 1, + "Remark": + {}, + "RemarkPyinitial": + {}, + "RemarkQuanPin": + {}, + "ContactType": 0, + "RoomInfoCount": 0, + "DomainList": [ + {}], + "ChatRoomNotify": 0, + "AddContactScene": 0, + "Province": "Jiangsu", + "City": "Nanjing", + "Signature": "......", + "PersonalCard": 0, + "HasWeiXinHdHeadImg": 1, + "VerifyFlag": 0, + "Level": 6, + "Source": 14, + "WeiboFlag": 0, + "AlbumStyle": 0, + "AlbumFlag": 3, + "SnsUserInfo": + { + "SnsFlag": 1, + "SnsBgimgId": "http://shmmsns.qpic.cn/mmsns/FzeKA69P5uIdqPfQxp59LvOohoE2iaiaj86IBH1jl0F76aGvg8AlU7giaMtBhQ3bPibunbhVLb3aEq4/0", + "SnsBgobjectId": 14216284872728580667, + "SnsFlagEx": 7297 + }, + "Country": "CN", + "BigHeadImgUrl": "https://wx.qlogo.cn/mmhead/ver_1/qqncCu2avRYruPcQbav3PrwaGSS31QgN6dqW8q1XuDKjgiaAuwoFPw3kN8Cj3zIBL36M93R2Xwib0IddUK3gqbFeezEiaA8K2mMdibT5VUDDrbn7F7M1Mxicmows9cdYNOicjI/0", + "SmallHeadImgUrl": "https://wx.qlogo.cn/mmhead/ver_1/qqncCu2avRYruPcQbav3PrwaGSS31QgN6dqW8q1XuDKjgiaAuwoFPw3kN8Cj3zIBL36M93R2Xwib0IddUK3gqbFeezEiaA8K2mMdibT5VUDDrbn7F7M1Mxicmows9cdYNOicjI/132", + "CustomizedInfo": + { + "BrandFlag": 0 + }, + "EncryptUserName": "v3_020b3826fd03010000000000feba078fc1e760000000501ea9a3dba12f95f6b60a0536a1adb6f6352c38d0916c9c74045d85aa602efa2d81b84adde05d285124e8a54b9fcd039f725d6ac0d3bd651c7c74503a@stranger", + "AdditionalContactList": + { + "LinkedinContactItem": + {} + }, + "ChatroomMaxCount": 0, + "DeleteFlag": 0, + "Description": "\b\u0000\u0018\u0000\"\u0000(\u00008\u0000", + "ChatroomStatus": 0, + "Extflag": 0, + "ChatRoomBusinessType": 0 + } +} +``` + + +#### 小程序消息 +- 判断此类消息的逻辑:\$.Data.MsgType=49 并且 解析\$.Data.Content.string中的xml msg.appmsg.type=33/36 +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356109, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 49, + "Content": + { + "string": "\n\n\t\n\t\t腾讯云助手\n\t\t腾讯云助手\n\t\t33\n\t\thttps://mp.weixin.qq.com/mp/waerrpage?appid=wxe2039b83454e49ed&type=upgrade&upgradetype=3#wechat_redirect\n\t\t\n\t\t\t3057020100044b304902010002048399cc8402032df731020414e461b4020465a0eb8f042463626430353633382d376263632d346161642d396234372d3435613131336339326231640204051808030201000405004c550500\n\t\t\te1284d4ae13ebd9bb2cde5251cdd05e4\n\t\t\t52357\n\t\t\t720\n\t\t\t576\n\t\t\td4142726bc730088f0fa44c9161a0992\n\t\t\td4142726bc730088f0fa44c9161a0992\n\t\t\t0\n\t\t\twxid_0xsqb3o0tsvz22_38_1705044879\n\t\t\n\t\tgh_44fc2ced7f87@app\n\t\t腾讯云助手\n\t\te1284d4ae13ebd9bb2cde5251cdd05e4\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t2\n\t\t\t594\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t0\n\t\t\t0\n\t\t\t538\n\t\t\t0\n\t\t\t0\n\t\t\t0\n\t\t\t0\n\t\t\n\t\n\twxid_phyyedw9xap22\n\t0\n\t\n\t\t1\n\t\t\n\t\n\t\n\n" + }, + "Status": 3, + "ImgStatus": 2, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705044879, 消息发送时间 + "MsgSource": "\n\t0\n\t\n\t\t2\n\t\n\t\n\t\tdb46d46fe0a926c4b571dfe9d8096bfa_\n\t\n\tv1_DkelOoZN\n\t\n\t\t\n\t\n\n", + "PushContent": "朝夕。 : [小程序]腾讯云助手", 消息通知内容 + "NewMsgId": 572974861799389774, 消息ID + "MsgSeq": 640356109 + } +} +``` + +#### 引用消息 +- 判断此类消息的逻辑:\$.Data.MsgType=49 并且 解析\$.Data.Content.string中的xml msg.appmsg.type=57 +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356110, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 49, + "Content": + { + "string": "\n\n\t\n\t\t看看这个\n\t\t\n\t\t\n\t\t57\n\t\t0\n\t\t0\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t0\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\t0\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\t49\n\t\t\t3617029648443513152\n\t\t\twxid_phyyedw9xap22\n\t\t\twxid_phyyedw9xap22\n\t\t\t朝夕。\n\t\t\t<msg><appmsg appid=\"\" sdkver=\"0\"><title>hhh.xlsx</title><des></des><action></action><type>6</type><showtype>0</showtype><soundtype>0</soundtype><mediatagname></mediatagname><messageext></messageext><messageaction></messageaction><content></content><contentattr>0</contentattr><url></url><lowurl></lowurl><dataurl></dataurl><lowdataurl></lowdataurl><appattach><totallen>8939</totallen><attachid>@cdn_3057020100044b304902010002043904752002032f7e350204aa0dd83a020465a0e897042430373538386564322d353866642d343234342d386563652d6236353536306438623936610204011800050201000405004c56f900_3f28b0cbd65a86c3a980f3e22808c0fe_1</attachid><emoticonmd5></emoticonmd5><fileext>xlsx</fileext><cdnattachurl>3057020100044b304902010002043904752002032f7e350204aa0dd83a020465a0e897042430373538386564322d353866642d343234342d386563652d6236353536306438623936610204011800050201000405004c56f900</cdnattachurl><aeskey>3f28b0cbd65a86c3a980f3e22808c0fe</aeskey><encryver>0</encryver><overwrite_newmsgid>1789783684714859663</overwrite_newmsgid><fileuploadtoken>v1_paVQtd+CWGr2I3eOg71E6KBpQf0yY9RFQkqDPwT4yMnnbawqveao1vAE0qCOhWcIPkMGZavimUTDFcImr+SaManD8pKVQbBPTUvSmA6UsXgZWqQDOT00VLx7U/hoP3/CwveN2Lk56nxcef/XJiGKrOpAHKHcZvccaGk9/68wsBCOyanya/9xgdHTYxyQp4IadiSe</fileuploadtoken></appattach><extinfo></extinfo><sourceusername></sourceusername><sourcedisplayname></sourcedisplayname><thumburl></thumburl><md5>84c6737fe9549270c9b3ca4f6fc88f6f</md5><statextstr></statextstr></appmsg><fromusername></fromusername><appinfo><version>0</version><appname></appname><isforceupdate>1</isforceupdate></appinfo></msg>\n\t\t\t<msgsource>\n\t<alnode>\n\t\t<cf>3</cf>\n\t</alnode>\n\t<sec_msg_node>\n\t\t<uuid>896374a2b5979141804d509256c22f0b_</uuid>\n\t</sec_msg_node>\n</msgsource>\n\n\t\t\n\t\n\twxid_phyyedw9xap22\n\t0\n\t\n\t\t1\n\t\t\n\t\n\t\n\n" + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705044946, 消息发送时间 + "MsgSource": "\n\t\n\t\tea25ade83dc4b9ec91060ca3e1a0f5a2_\n\t\n\tv1_oTWRYdd1\n\t\n\t\t\n\t\n\n", + "PushContent": "看看这个", 消息通知内容 + "NewMsgId": 4334300109515885085, 消息ID + "MsgSeq": 640356110 + } +} +``` + +#### 转账消息 +- 判断此类消息的逻辑:\$.Data.MsgType=49 并且 解析\$.Data.Content.string中的xml msg.appmsg.type=2000 +```json + { + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356112, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 49, + "Content": + { + "string": "\n\n<![CDATA[微信转账]]>\n\n\n2000\n\n\n\n\n\n\n\n1\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705044984, 消息发送时间 + "MsgSource": "\n\tv1_eDcIna+F\n\t\n\t\t\n\t\n\n", + "PushContent": "朝夕。 : [转账]", 消息通知内容 + "NewMsgId": 7290406378327063279, 消息ID + "MsgSeq": 640356112 + } + } +``` + +#### 红包消息 +- 判断此类消息的逻辑:\$.Data.MsgType=49 并且 解析\$.Data.Content.string中的xml msg.appmsg.type=2001 +```json + { + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356113, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 49, + "Content": + { + "string": "\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t<![CDATA[微信红包]]>\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t微信红包\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\n" + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045011, 消息发送时间 + "MsgSource": "\n\t\n\t\n\tv1_Js6wJde/\n\t\n\t\t\n\t\n\n", + "PushContent": "朝夕。 : [红包]恭喜发财,大吉大利", 消息通知内容 + "NewMsgId": 5517720959405775296, 消息ID + "MsgSeq": 640356113 + } + } +``` + +#### 视频号消息 +- 判断此类消息的逻辑:\$.Data.MsgType=49 并且 解析\$.Data.Content.string中的xml msg.appmsg.type=51 +```json + { + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356115, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 49, + "Content": + { + "string": "\n\n\t\n\t\t当前微信版本不支持展示该内容,请升级至最新版本。\n\t\t\n\t\t\n\t\t51\n\t\t0\n\t\t0\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t0\n\t\thttps://support.weixin.qq.com/update/\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\t0\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\t14264358459626428566\n\t\t\t4\n\t\t\t国风锦鲤\n\t\t\thttps://wx.qlogo.cn/finderhead/ver_1/x2LxetmLmgoo9jp69R3wcrtZ0LBLdjVv9vrK9HmPNGEdD1iawdrPffPvMmFUez8pWqRIfs7DtgPiaV5C7DZpibH8b3y0jG178aIict6uPf0Vht4/0\n\t\t\t还招人么?我不要工资#逆水寒cos\n\t\t\t1\n\t\t\t8046877030770906689_0_0_0_0_0\n\t\t\t0\n\t\t\tv2_060000231003b20faec8cae08b19c7d2c702e834b077fb74f482543ff67f0cc66363057a5443@finder\n\t\t\t\n\t\t\t0\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t4\n\t\t\t\t\thttp://wxapp.tc.qq.com/251/20302/stodownload?encfilekey=Cvvj5Ix3eez3Y79SxtvVL0L7CkPM6dFibFeI6caGYwFFDAZJzcvicKz3jic4UfNeiaWTwH9gTlYiafAxVkMZRXicBUBk2Ms7lauAj6SArUu0P9ddKiaa8IWZzYaaKLf1WddH4G8T0KicxQV3hQPH3pQgEMTscw&a=1&bizid=1023&dotrans=0&hy=SH&idx=1&m=4c4c7f3ed03a14a6b99d0d19176c12ac&upid=290110\n\t\t\t\t\thttp://wxapp.tc.qq.com/251/20304/stodownload?encfilekey=oibeqyX228riaCwo9STVsGLPj9UYCicgttvO59vjtcQ7Jviaia0q4bnpVP2ia7ibqzacPo0z4nIRtWom80ZXwL64icZO2q6ibVBQLZQftMwU3SHj5uplsIFroHeF0QNcCkXX3RtibaWCHJQjfqZUk&bizid=1023&dotrans=0&hy=SH&idx=1&m=7522250b4d15e5df866bf23da9f117d6&token=oA9SZ4icv8IssuhLtacX13nAzXiaf8y52juKW4ibUDN7a2vn71bbrCR0LZiabddvTsLLMvnELnuAwNxViclRT7wT9IyibzFw1pq9wdichRYaEmb6Js&ctsc=2-20\n\t\t\t\t\t1080\n\t\t\t\t\t1920\n\t\t\t\t\thttp://wxapp.tc.qq.com/251/20304/stodownload?encfilekey=oibeqyX228riaCwo9STVsGLPj9UYCicgttvO59vjtcQ7Jviaia0q4bnpVP2ia7ibqzacPo0z4nIRtWom80ZXwL64icZO2q6ibVBQLZQftMwU3SHj5uplsIFroHeF0QNcCkXX3RtibaWCHJQjfqZUk&bizid=1023&dotrans=0&hy=SH&idx=1&m=7522250b4d15e5df866bf23da9f117d6&token=oA9SZ4icv8IssuhLtacX13nAzXiaf8y52juKW4ibUDN7a2vn71bbrCR0LZiabddvTsLLMvnELnuAwNxViclRT7wT9IyibzFw1pq9wdichRYaEmb6Js&ctsc=2-20\n\t\t\t\t\thttp://wxapp.tc.qq.com/251/20350/stodownload?encfilekey=oibeqyX228riaCwo9STVsGLPj9UYCicgttv1FCQXwResqN75zI4n65zY5tkAficEPWbbClq2VcicqMYaSLK7nrAVMasrIhvsCXJib5cOLib98JgWPr4SP92W6YEkVN5Uv0TKAdyRryQ3Qxk7jU&bizid=1023&dotrans=0&hy=SH&idx=1&m=731b89683dd3cb866cdf96dab70ac183&token=KkOFht0mCXlnmicFbJnvymIJOEfZgzia8PY0ZzOdaIYTJXwfblvK4U1ibntribm1beupHwictGWs9hpMiclyhfSb6766Lnb3ib0j14bENm6u1tHpeo&ctsc=3-20\n\t\t\t\t\t10>>\n\t\t\t\t\n\t\t\t\n\t\t\n\t\n\twxid_phyyedw9xap22\n\t0\n\t\n\t\t1\n\t\t\n\t\n\t\n\n" + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045057, 消息发送时间 + "MsgSource": "\n\t\n\t\t\n\t\n\t\n\t\t4\n\t\n\t\n\t\tbb2cbd9d3290e7a3d35f183eaade2213_\n\t\n\tv1_+Tfo41HS\n\n", + "PushContent": "你收到了一条消息", 消息通知内容 + "NewMsgId": 5576224237104747184, 消息ID + "MsgSeq": 640356115 + } + } +``` + +#### 撤回消息 +- 判断此类消息的逻辑:\$.Data.MsgType=10002 并且 解析\$.Data.Content.string中的xml sysmsg.type=revokemsg +```json + { + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356116, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 10002, + "Content": + { + "string": "wxid_phyyedw9xap2210403561155576224237104747184" + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045083, 消息发送时间 + "MsgSource": "\n\t\n\t\t\n\t\n\n", + "NewMsgId": 1968256046, 消息ID + "MsgSeq": 640356116 + } + } +``` + +#### 拍一拍消息 +- 判断此类消息的逻辑:\$.Data.MsgType=10002 并且 解析\$.Data.Content.string中的xml sysmsg.type=pat +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356117, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 10002, + "Content": + { + "string": "\n\n wxid_phyyedw9xap22\n wxid_0xsqb3o0tsvz22\n wxid_0xsqb3o0tsvz22\n \n 0\n\n\n\n\n \n\n\n\n\n\n" + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045115, 消息发送时间 + "MsgSource": "\n\t\n\t\t\n\t\n\n", + "NewMsgId": 5709690173850254331, 消息ID + "MsgSeq": 640356117 + } +} +``` + +#### 地理位置 +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356118, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 48, 消息类型,48是地理位置消息 + "Content": + { + "string": "\n\n\t\n\n" + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045153, 消息发送时间 + "MsgSource": "\n\t0\n\tv1_KgQA8C+H\n\t\n\t\t\n\t\n\n", + "PushContent": "朝夕。分享了一个地理位置", 消息通知内容 + "NewMsgId": 2112726776406556053, 消息ID + "MsgSeq": 640356118 + } +} +``` + +#### 群聊邀请 +- 判断此类消息的逻辑:\$.Data.MsgType=49 并且 解析\$.Data.Content.string中的xml msg.appmsg.title=邀请你加入群聊(根据手机设置的系统语言title会有调整,不同语言关键字不同) +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356119, 消息ID + "FromUserName": + { + "string": "wxid_phyyedw9xap22" 消息发送人的wxid + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 49, + "Content": + { + "string": "<![CDATA[邀请你加入群聊]]>view500" + }, + "Status": 3, + "ImgStatus": 0, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045206, 消息发送时间 + "MsgSource": "\n\tv1_uHiWbihr\n\t\n\t\t\n\t\n\n", + "NewMsgId": 2331390497668538400, 消息ID + "MsgSeq": 640356119 + } +} +``` + +#### 被移除群聊通知 +- 判断此类消息的逻辑:\$.Data.MsgType=10000 并且 \$.Data.Content.string内容为移除群聊的通知内容 +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356153, 消息ID + "FromUserName": + { + "string": "39238473509@chatroom" 所在群聊的ID + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 10000, + "Content": + { + "string": "你被\"朝夕。\"移出群聊" + }, + "Status": 4, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045790, 消息发送时间 + "MsgSource": "\n\tv1_f7Xny9H/\n\t\n\t\t\n\t\n\n", + "NewMsgId": 5759605552965664254, 消息ID + "MsgSeq": 640356153 + } +} +``` + +#### 踢出群聊通知 +- 判断此类消息的逻辑:\$.Data.MsgType=10002 并且 解析\$.Data.Content.string中的xml sysmsg.type=sysmsgtemplate 并且 template中的内容为“你将xxx移出了群聊”(根据手机设置的系统语言template会有调整,不同语言关键字不同) +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356143, 消息ID + "FromUserName": + { + "string": "34757816141@chatroom" 所在群聊的ID + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 10002, + "Content": + { + "string": "34757816141@chatroom:\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t\n\n" + }, + "Status": 4, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045666, 消息发送时间 + "MsgSource": "\n\t\n\t\t\n\t\n\n", + "NewMsgId": 7100572668516374210, 消息ID + "MsgSeq": 640356143 + } +} +``` + +#### 解散群聊通知 +- 判断此类消息的逻辑:\$.Data.MsgType=10002 并且 解析\$.Data.Content.string中的xml sysmsg.type=sysmsgtemplate 并且 template中的内容为“群主xxx已解散该群聊”(根据手机设置的系统语言template会有调整,不同语言关键字不同) +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356158, 消息ID + "FromUserName": + { + "string": "39238473509@chatroom" 所在群聊的ID + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 10002, + "Content": + { + "string": "39238473509@chatroom:\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n" + }, + "Status": 4, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045834, 消息发送时间 + "MsgSource": "\n\t\n\t\t\n\t\n\n", + "NewMsgId": 6869316888754169027, 消息ID + "MsgSeq": 640356158 + } +} +``` + +#### 修改群名称 +- 判断此类消息的逻辑:\$.Data.MsgType=10000 并且 \$.Data.Content.string为修改群名的通知内容 +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356129, 消息ID + "FromUserName": + { + "string": "34757816141@chatroom" 所在群聊的ID + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 10000, + "Content": + { + "string": "你修改群名为“GeWe test1”" + }, + "Status": 4, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045517, 消息发送时间 + "MsgSource": "\n\tv1_3uPmlxJG\n\t\n\t\t\n\t\n\n", + "NewMsgId": 6984814725261047392, 消息ID + "MsgSeq": 640356129 + } +} +``` + +#### 更换群主通知 +- 判断此类消息的逻辑:\$.Data.MsgType=10000 并且 \$.Data.Content.string为更换群主的通知内容 +```json + { + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356125, 消息ID + "FromUserName": + { + "string": "34757816141@chatroom" 所在群聊的ID + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 消息接收人的wxid + }, + "MsgType": 10000, + "Content": + { + "string": "你已成为新群主" + }, + "Status": 4, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045441, 消息发送时间 + "MsgSource": "\n\tv1_iqIx6JkV\n\t\n\t\t\n\t\n\n", + "NewMsgId": 7268255507978211143, 消息ID + "MsgSeq": 640356125 + } + } +``` + +#### 群信息变更通知 +```json +{ + "TypeName": "ModContacts", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "UserName": + { + "string": "34757816141@chatroom" 所在群聊的ID + }, + "NickName": + { + "string": "GeWe test" + }, + "PyInitial": + { + "string": "GEWETEST" + }, + "QuanPin": + { + "string": "GeWetest" + }, + "Sex": 0, + "ImgBuf": + { + "iLen": 0 + }, + "BitMask": 4294967295, + "BitVal": 2, + "ImgFlag": 1, + "Remark": + {}, + "RemarkPyinitial": + {}, + "RemarkQuanPin": + {}, + "ContactType": 0, + "RoomInfoCount": 0, + "DomainList": [ + {}], + "ChatRoomNotify": 1, + "AddContactScene": 0, + "PersonalCard": 0, + "HasWeiXinHdHeadImg": 0, + "VerifyFlag": 0, + "Level": 0, + "Source": 0, + "ChatRoomOwner": "wxid_0xsqb3o0tsvz22", + "WeiboFlag": 0, + "AlbumStyle": 0, + "AlbumFlag": 0, + "SnsUserInfo": + { + "SnsFlag": 0, + "SnsBgobjectId": 0, + "SnsFlagEx": 0 + }, + "CustomizedInfo": + { + "BrandFlag": 0 + }, + "AdditionalContactList": + { + "LinkedinContactItem": + {} + }, + "ChatroomMaxCount": 700000019, + "DeleteFlag": 2, + "Description": "\b\u0004\u0012\u0017\n\u000Ewxid_phyyedw9xap220\u0001@\u0000�\u0001\u0000\u0012\u001B\n\u0012wxid_phyyedw9xap220\u0001@\u0000�\u0001\u0000\u0012\u001C\n\u0013wxid_0xsqb3o0tsvz220\u0001@\u0000�\u0001\u0000\u0012\u001D\n\u0013wxid_8pvka4jg6qzt220�\u0010@\u0000�\u0001\u0000\u0018\u0001\"\u0000(\u00008\u0000", + "ChatroomStatus": 27, + "Extflag": 0, + "ChatRoomBusinessType": 0 + } +} +``` + +#### 发布群公告 +- 判断此类消息的逻辑:\$.Data.MsgType=10002 并且 解析\$.Data.Content.string中的xml sysmsg.type=mmchatroombarannouncememt +```json +{ + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356133, 消息ID + "FromUserName": + { + "string": "wxid_0xsqb3o0tsvz22" 发布人的wxid + }, + "ToUserName": + { + "string": "34757816141@chatroom" 所在群聊的ID + }, + "MsgType": 10002, + "Content": + { + "string": "\n \n \n \n\t1705045558\n\t127\n\t1\n\t\n\t\twxid_0xsqb3o0tsvz22\n\t\t34757816141@chatroom\n\t\t7c79fed82a0037648954bba6d5ca2025\n\t\n\t\n\t\t\n\t\t\t.htm\n\t\t\thttp://wxapp.tc.qq.com/264/20303/stodownload?m=145a874d4eb1bb0b85af928331a168aa&filekey=3033020101041f301d02020108040253480410145a874d4eb1bb0b85af928331a168aa020120040d00000004627466730000000132&hy=SH&storeid=265a0ee36000a9c94f3064bb50000010800004f4f534825960b01e676a0b3b&bizid=1023\n\t\t\t24808ae91ac7d636c99a1b340a1f9253\n\t\t\t8fac8374ded0d5e8d5038b1ec2b77a62\n\t\t\tef033738f28bb3c80cd5e7290fdbfdcf\n\t\t\tef033738f28bb3c80cd5e7290fdbfdcf\n\t\t\t20\n\t\t\n\t\t\n\t\t\t群公告哈1\n\t\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\t-1\n\t\t\n\t\n\twxid_0xsqb3o0tsvz22_34757816141@chatroom_1705045558_2028281562\n\n]]>\n \n \n" + }, + "Status": 3, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045559, 消息发送时间 + "MsgSource": "\n\t\n\t\t\n\t\n\n", + "NewMsgId": 8056409355261218186, 消息ID + "MsgSeq": 640356133 + } +} +``` + +#### 群待办 +- 判断此类消息的逻辑:\$.Data.MsgType=10002 并且 解析\$.Data.Content.string中的xml sysmsg.type=roomtoolstips +```json + { + "TypeName": "AddMsg", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "MsgId": 1040356135, 消息ID + "FromUserName": + { + "string": "34757816141@chatroom" 所在群聊的ID + }, + "ToUserName": + { + "string": "wxid_0xsqb3o0tsvz22" + }, + "MsgType": 10002, + "Content": + { + "string": "34757816141@chatroom:\n\n\n 0\n\n \n \n \n \n \n <![CDATA[群公告]]>\n \n \n \n 0\n \n \n \n \n\n\n \n\n\n \n \n \n \n \n \n \n\n \n\n \n\n\n" + }, + "Status": 4, + "ImgStatus": 1, + "ImgBuf": + { + "iLen": 0 + }, + "CreateTime": 1705045591, 消息发送时间 + "MsgSource": "\n\t\n\t\t\n\t\n\n", + "NewMsgId": 1765700414095721113, 消息ID + "MsgSeq": 640356135 + } + } +``` + +#### 删除好友通知 +```json +{ + "TypeName": "DelContacts", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "UserName": + { + "string": "wxid_phyyedw9xap22" 删除的好友wxid + }, + "DeleteContactScen": 0 + } +} +``` + +#### 退出群聊 +```json +{ + "TypeName": "DelContacts", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22", 所属微信的wxid + "Data": + { + "UserName": + { + "string": "34559815390@chatroom" 退出的群聊ID + }, + "DeleteContactScen": 0 + } +} +``` + +#### 掉线通知 +```json +{ + "TypeName": "Offline", 消息类型 + "Appid": "wx_wR_U4zPj2M_OTS3BCyoE4", 设备ID + "Wxid": "wxid_phyyedw9xap22" 掉线号的wxid +} +``` diff --git a/gewechat/client/get_token.py b/gewechat/client/get_token.py new file mode 100644 index 0000000..b7af7d4 --- /dev/null +++ b/gewechat/client/get_token.py @@ -0,0 +1,10 @@ +import requests + +url = "/tools/getTokenId" + +payload={} +headers = {} +base_url="http://192.168.2.240:2531/v2/api" +response = requests.request("POST", base_url+url, headers=headers, data=payload) + +print(response.text) \ No newline at end of file diff --git a/gewechat/client/login.py b/gewechat/client/login.py new file mode 100644 index 0000000..0f41096 --- /dev/null +++ b/gewechat/client/login.py @@ -0,0 +1,12 @@ + +import requests +def login(): + + url = "/tools/getTokenId" + + payload = {} + headers = {} + base_url = "http://192.168.2.240:2531/v2/api" + response = requests.request("POST", base_url + url, headers=headers, data=payload) + + print(response.text) \ No newline at end of file diff --git a/main.py b/main.py index 83704aa..153ab83 100644 --- a/main.py +++ b/main.py @@ -1,35 +1,126 @@ #! /usr/bin/env python3 # -*- coding: utf-8 -*- - -import signal +import logging import threading from argparse import ArgumentParser +import uvicorn +from fastapi import FastAPI + +from gewechat_client import GewechatClient + +import socket +# 启动FastAPI服务器 +# 从callback_url中提取主机和端口 +import urllib.parse from configuration import Config from constants import ChatType -from robot import Robot, __version__ -from wcferry import Wcf +from robot import Robot +from gewechat.api.callback import router as callback_router + +# 配置日志 +logger = logging.getLogger(__name__) + + +def is_port_in_use(port, host='0.0.0.0'): + """检查端口是否被占用""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.bind((host, port)) + return False + except socket.error: + return True + + +def start_fastapi_server(host="0.0.0.0", port=8999): + """启动FastAPI服务器""" + # 检查端口是否被占用 + if is_port_in_use(port, host): + logger.warning(f"端口 {port} 已被占用,尝试使用其他端口") + # 尝试其他端口 + for test_port in range(9000, 9100): + if not is_port_in_use(test_port, host): + port = test_port + break + else: + logger.error("无法找到可用端口,服务器启动失败") + return False + + try: + app = FastAPI() + app.include_router(callback_router) + + # 添加健康检查路由 + @app.get("/health") + async def health_check(): + return {"status": "ok"} + + logger.info(f"正在启动FastAPI服务器,地址: http://{host}:{port}") + + # 使用线程启动uvicorn服务器 + server_thread = threading.Thread( + target=uvicorn.run, + args=(app,), + kwargs={"host": host, "port": port, "log_level": "info"}, + daemon=True + ) + server_thread.start() + logger.info(f"FastAPI 服务已在 http://{host}:{port} 启动") + logger.info(f"回调URL: http://{host}:{port}/gewechat/callback") + + # 返回启动的端口,以便调用者知道实际使用的端口 + return port + except Exception as e: + logger.error(f"启动FastAPI服务器失败: {e}", exc_info=True) + return False + def main(chat_type: int): config = Config() - wcf = Wcf(debug=True) + base_url = config.BASE_URL + token = config.GEWECHAT_TOKEN + app_id = config.APP_ID + callback_url = config.CALLBACK_URL + send_msg_wxid = "filehelper" # 要发送消息的好友昵称 - def handler(sig, frame): - # 在退出前先关闭插件系统 - robot.plugin_manager.shutdown_plugins() - wcf.cleanup() # 退出前清理环境 - exit(0) + parsed_url = urllib.parse.urlparse(callback_url) + host = parsed_url.hostname or "0.0.0.0" + port = parsed_url.port or 8999 - signal.signal(signal.SIGINT, handler) + # start_fastapi_server(host, port) - robot = Robot(config, wcf, chat_type) - robot.LOG.info(f"WeChatRobot【{__version__}】成功启动···") + # 创建 GewechatClient 实例 + client = GewechatClient(base_url, token) + + # 登录, 自动创建二维码,扫码后自动登录 + app_id, error_msg = client.login(app_id=app_id) + + if error_msg: + print("登录失败") + return + + resp = client.set_callback(token, callback_url) + print(f"set_callback:{resp}") + + # 如果启动时,配置文件中的app_id为空,那么将app_id写入配置文件 + if not config.APP_ID: + # 更新配置文件中的APP_ID + config.update_config('gewechat', 'app_id', app_id) + print(f"已将新的APP_ID: {app_id} 写入配置文件") + # 同时更新当前配置对象中的APP_ID + config.APP_ID = app_id + + # 创建机器人实例 + robot = Robot(config, app_id, client, chat_type) + robot.LOG.info(f"WeChatRobot gewechat 成功启动···") + + # # 注册Robot实例到callback模块 + # from gewechat.api.callback import register_robot + # register_robot(app_id, robot) # 机器人启动发送测试消息 - robot.send_text_msg("启动成功!", "filehelper") + client.post_text(app_id, send_msg_wxid, "gewechat client 启动成功!") - # 接收消息 - robot.enableReceivingMsg() # 加队列 # 每天 8:30 发送新闻 robot.onEveryTime("08:30", robot.news_baidu_report_auto) @@ -52,21 +143,22 @@ def main(chat_type: int): # 启动Dashboard服务器 dashboard_server = None - try: - # 创建Dashboard服务器实例,共享robot对象 - from admin.dashboard.server import DashboardServer - dashboard_server = DashboardServer(robot_instance=robot) - - # 在单独的线程中启动Dashboard服务器 - dashboard_thread = threading.Thread(target=dashboard_server.run, daemon=True) - dashboard_thread.start() - robot.LOG.info(f"Dashboard服务器已在 http://{dashboard_server.host}:{dashboard_server.port} 启动") - except Exception as e: - robot.LOG.error(f"Dashboard服务器启动失败: {e}") + # try: + # # 创建Dashboard服务器实例,共享robot对象 + # from admin.dashboard.server import DashboardServer + # dashboard_server = DashboardServer(robot_instance=robot) + # + # # 在单独的线程中启动Dashboard服务器 + # dashboard_thread = threading.Thread(target=dashboard_server.run, daemon=True) + # dashboard_thread.start() + # robot.LOG.info(f"Dashboard服务器已在 http://{dashboard_server.host}:{dashboard_server.port} 启动") + # except Exception as e: + # robot.LOG.error(f"Dashboard服务器启动失败: {e}") # 让机器人一直跑 robot.keep_running_and_block_process() + if __name__ == "__main__": parser = ArgumentParser() parser.add_argument('-c', type=int, default=0, help=f'选择模型参数序号: {ChatType.help_hint()}') diff --git a/message_util.py b/message_util.py index 057f266..0b54765 100644 --- a/message_util.py +++ b/message_util.py @@ -1,10 +1,13 @@ # -*- coding: utf-8 -*- import logging +import os.path import random import time from typing import Optional -from wcferry import Wcf +from gewechat_client import GewechatClient + +from utils.wechat.contact_manager import ContactManager class MessageUtil: @@ -13,15 +16,16 @@ class MessageUtil: """ # 修改 MessageUtil 类的初始化方法,接受联系人管理器而不是联系人字典 - def __init__(self, wcf, contact_manager): - self.wcf = wcf + def __init__(self, app_id: str, base_url: str, client: GewechatClient, contact_manager: ContactManager): + self.app_id = app_id + self.client = client self.contact_manager = contact_manager self.LOG = logging.getLogger("MessageUtil") def send_text(self, msg: str, receiver: str, at_list: str = "") -> None: """ 发送文本消息 - + :param msg: 消息字符串 :param receiver: 接收人wxid或者群id :param at_list: 要@的wxid, @所有人的wxid为:notify@all @@ -35,22 +39,21 @@ class MessageUtil: ats = " @所有人" else: wxids = at_list.split(",") - for wxid in wxids: - # 根据 wxid 查找群昵称 - ats += f" @{self.wcf.get_alias_in_chatroom(wxid, receiver)}" + if len(wxids) > 0: + ats += self.get_user_chatroom_nickname(receiver, wxids) # {msg}{ats} 表示要发送的消息内容后面紧跟@,例如 北京天气情况为:xxx @张三 if ats == "": self.LOG.info(f"To {receiver}: {msg}") - self.wcf.send_text(f"{msg}", receiver, at_list) + self.client.post_text(self.app_id, receiver, "{msg}", "") else: self.LOG.info(f"To {receiver}: {ats}\r{msg}") - self.wcf.send_text(f"{ats}\n{msg}", receiver, at_list) + self.client.post_text(self.app_id, receiver, f"{ats}\n{msg}", at_list) - def send_file(self, file_path: str, receiver: str) -> None: + def send_file(self, file_path: str, receiver: str) -> str: """ 发送文件消息 - + :param file_path: 文件路径 :param receiver: 接收人wxid或者群id """ @@ -58,9 +61,11 @@ class MessageUtil: time.sleep(random.uniform(0.5, 1.5)) self.LOG.info(f"Sending file to {receiver}: {file_path}") - self.wcf.send_file(file_path, receiver) + (path, filename) = os.path.split(file_path) + self.LOG.info(f"Sending file to {path}: {filename}") + return self.client.post_file(self.app_id, receiver, file_path, filename) - def send_image(self, image_path: str, receiver: str) -> None: + def send_image(self, image_path: str, receiver: str) -> str: """ 发送文件消息 @@ -71,13 +76,13 @@ class MessageUtil: time.sleep(random.uniform(0.5, 1.5)) self.LOG.info(f"Sending file to {receiver}: {image_path}") - self.wcf.send_image(image_path, receiver) + return self.client.post_image(self.app_id, receiver, image_path) def send_rich_text(self, name: str, account: str, title: str, digest: str, url: str, thumburl: str, receiver: str) -> int: """ 发送富文本消息 - + 卡片样式: |-------------------------------------| |title, 最长两行 | @@ -87,7 +92,7 @@ class MessageUtil: |digest, 最多三行,会占位 |--------| |(account logo) name | |-------------------------------------| - + :param name: 左下显示的名字 :param account: 填公众号 id 可以显示对应的头像(gh_ 开头的) :param title: 标题,最多两行 @@ -101,17 +106,21 @@ class MessageUtil: time.sleep(random.uniform(0.5, 1.5)) self.LOG.info(f"Sending rich text to {receiver}: {title}") - return self.wcf.send_rich_text(name, account, title, digest, url, thumburl, receiver) + return self.client.post_link(self.app_id, receiver, title, digest, url, thumburl) - def update_contacts(self, contacts: dict) -> None: - """ - 更新联系人字典 - - :param contacts: 联系人字典,格式为 {"wxid": "NickName"} - """ - self.contacts.update(contacts) + def get_user_chatroom_nickname(self, chatroom_id: str, member_wxids: list[str]) -> str: + data = self.client.get_chatroom_member_detail(self.app_id, chatroom_id, member_wxids) + nicknames_with_at = [" @" + member["nickName"] for member in data["data"] if member.get("nickName")] + return " ".join(nicknames_with_at) - # 修改使用 allContacts 的地方,改为使用 contact_manager - # 例如: - # 原来的代码: nickname = self.allContacts.get(wxid, wxid) - # 修改为: nickname = self.contact_manager.get_nickname(wxid) + def invite_member(self, group_id, sender): + return self.client.invite_member(self.app_id, sender, group_id, "自动加群邀请") + + def get_chatroom_members(self, group_id) -> dict: + data = self.client.get_chatroom_member_list(self.app_id, group_id) + members = {member["wxid"]: member["nickName"] for member in data["data"]["memberList"]} + return members + + def download_file_from_url(self, url: str, target_dir: str) -> str: + # 根据获取的文件地址,从server 下载 :http://{服务ip}:2532/download/{接口返回的文件路径} + return "" diff --git a/plugins/beautyleg/main.py b/plugins/beautyleg/main.py index 50f7dfd..786ab76 100644 --- a/plugins/beautyleg/main.py +++ b/plugins/beautyleg/main.py @@ -117,7 +117,7 @@ class BeautyLegPlugin(MessagePluginInterface): # 发送图片 random_file_path = os.path.abspath(random_file_path) self.LOG.info(f"BeautyLeg.random_file_path: {random_file_path}") - result = wcf.send_file(random_file_path, (roomid if roomid else sender)) + result = self.message_util.send_file(random_file_path, (roomid if roomid else sender)) self.LOG.info(f"发送图片结果: {result}") return True, "发送成功" diff --git a/plugins/dify/main.py b/plugins/dify/main.py index a72c609..52beb36 100644 --- a/plugins/dify/main.py +++ b/plugins/dify/main.py @@ -6,7 +6,6 @@ import time import re # 添加re模块导入 from typing import Dict, Any, List, Optional, Tuple -from wcferry import Wcf from message_util import MessageUtil from plugin_common.message_plugin_interface import MessagePluginInterface @@ -63,7 +62,6 @@ class DifyPlugin(MessagePluginInterface): self.LOG.info(f"正在初始化 {self.name} 插件...") # 保存上下文对象 - self.wcf = context.get("wcf") self.event_system = context.get("event_system") self.message_util: MessageUtil = context.get("message_util") self.gbm = context.get("gbm") diff --git a/plugins/douyin_parser/main.py b/plugins/douyin_parser/main.py index ef3f399..c3790ef 100644 --- a/plugins/douyin_parser/main.py +++ b/plugins/douyin_parser/main.py @@ -6,8 +6,7 @@ import traceback import requests from typing import Dict, Any, List, Optional, Tuple -from wcferry import Wcf - +from message_util import MessageUtil from plugin_common.message_plugin_interface import MessagePluginInterface from plugin_common.plugin_interface import PluginStatus from utils.decorator.plugin_decorators import plugin_stats_decorator @@ -61,9 +60,8 @@ class DouyinParserPlugin(MessagePluginInterface): self.LOG.info(f"正在初始化 {self.name} 插件...") # 保存上下文对象 - self.wcf = context.get("wcf") self.event_system = context.get("event_system") - self.message_util = context.get("message_util") + self.message_util:MessageUtil = context.get("message_util") self.gbm = context.get("gbm") # 从配置中获取参数 @@ -103,7 +101,6 @@ class DouyinParserPlugin(MessagePluginInterface): self.LOG.info(f"插件执行: {self.name}:{content}") sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") gbm: GroupBotManager = message.get("gbm") # 检查权限 @@ -138,14 +135,14 @@ class DouyinParserPlugin(MessagePluginInterface): # 下载并发送文件 mp4_path = self._download_stream(video_url, os.path.join(self.download_dir, "douyin.mp4")) if mp4_path: - wcf.send_file(mp4_path, (roomid if roomid else sender)) + self.message_util.send_file(mp4_path, (roomid if roomid else sender)) return True, "发送视频文件成功" else: print(f"❌下载视频失败") return False, "下载视频失败" else: # 发送卡片 - wcf.send_rich_text( + self.message_util.send_rich_text( "BOT-PC直接查看", "gh_11", title[:30], diff --git a/plugins/game_task/main.py b/plugins/game_task/main.py index 2193f2b..1836b3f 100644 --- a/plugins/game_task/main.py +++ b/plugins/game_task/main.py @@ -3,8 +3,6 @@ import logging from datetime import datetime from typing import Dict, Any, List, Optional, Tuple -from wcferry import Wcf - from message_util import MessageUtil from plugin_common.message_plugin_interface import MessagePluginInterface from plugin_common.plugin_interface import PluginStatus @@ -52,7 +50,6 @@ class GameTaskPlugin(MessagePluginInterface): self.LOG.info(f"正在初始化 {self.name} 插件...") # 保存上下文对象 - self.wcf = context.get("wcf") self.event_system = context.get("event_system") self.message_util: MessageUtil = context.get("message_util") @@ -115,7 +112,6 @@ class GameTaskPlugin(MessagePluginInterface): command = content.split(" ")[0].lower() sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") gbm: GroupBotManager = message.get("gbm") all_contacts = message.get("all_contacts", {}) diff --git a/plugins/global_news/main.py b/plugins/global_news/main.py index e6efe51..ec45a59 100644 --- a/plugins/global_news/main.py +++ b/plugins/global_news/main.py @@ -4,8 +4,7 @@ import threading import time # 添加这一行 from typing import Dict, Any, List, Optional, Tuple -from wcferry import Wcf - +from message_util import MessageUtil from plugin_common.message_plugin_interface import MessagePluginInterface from plugin_common.plugin_interface import PluginStatus from utils.decorator.plugin_decorators import plugin_stats_decorator @@ -55,12 +54,13 @@ class GlobalNewsPlugin(MessagePluginInterface): self.LOG.info(f"正在初始化 {self.name} 插件...") # 保存上下文对象 - self.wcf = context.get("wcf") self.event_system = context.get("event_system") - self.message_util = context.get("message_util") + self.message_util: MessageUtil = context.get("message_util") - self._commands = self._config.get("GlobalNews", {}).get("command", ["全球新闻", "国际新闻", "环球新闻", "政经新闻"]) - self.command_format = self._config.get("GlobalNews", {}).get("command-format", "全球新闻 - 获取最新的全球政治经济新闻") + self._commands = self._config.get("GlobalNews", {}).get("command", + ["全球新闻", "国际新闻", "环球新闻", "政经新闻"]) + self.command_format = self._config.get("GlobalNews", {}).get("command-format", + "全球新闻 - 获取最新的全球政治经济新闻") self.enable = self._config.get("GlobalNews", {}).get("enable", True) self.LOG.info(f"[{self.name}] 插件初始化完成,指令:{self._commands}") @@ -96,7 +96,6 @@ class GlobalNewsPlugin(MessagePluginInterface): self.LOG.info(f"插件执行: {self.name}:{content}") sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") gbm: GroupBotManager = message.get("gbm") # 检查权限 @@ -105,48 +104,48 @@ class GlobalNewsPlugin(MessagePluginInterface): # 生成唯一任务ID task_id = f"{sender}_{roomid}_{int(time.time())}" - + # 发送等待消息 - wcf.send_text("🌍正在获取全球新闻,请稍候...", - (roomid if roomid else sender), sender) - + self.message_util.send_text("🌍正在获取全球新闻,请稍候...", + (roomid if roomid else sender), sender) + # 启动异步任务 - self._start_news_task(task_id, sender, roomid, wcf) - + self._start_news_task(task_id, sender, roomid) + return True, "新闻获取任务已启动" - def _start_news_task(self, task_id: str, sender: str, roomid: str, wcf: Wcf): + def _start_news_task(self, task_id: str, sender: str, roomid: str): """启动异步新闻获取任务""" thread = threading.Thread( target=self._fetch_news_thread, - args=(task_id, sender, roomid, wcf) + args=(task_id, sender, roomid) ) thread.daemon = True thread.start() self._news_tasks[task_id] = thread self.LOG.info(f"启动新闻获取任务: {task_id}") - def _fetch_news_thread(self, task_id: str, sender: str, roomid: str, wcf: Wcf): + def _fetch_news_thread(self, task_id: str, sender: str, roomid: str): """在单独的线程中运行异步新闻获取任务""" try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) news_result = loop.run_until_complete(self._fetch_news_async()) loop.close() - + # 处理结果 if news_result: # 发送新闻图片 receiver = roomid if roomid else sender - wcf.send_image(news_result, receiver) - wcf.send_text("🌍全球新闻获取完成!", receiver, sender) + self.message_util.send_image(news_result, receiver) + self.message_util.send_text("🌍全球新闻获取完成!", receiver, sender) else: - wcf.send_text("❌获取新闻失败,请稍后再试", - (roomid if roomid else sender), sender) + self.message_util.send_text("❌获取新闻失败,请稍后再试", + (roomid if roomid else sender), sender) except Exception as e: self.LOG.error(f"新闻获取任务出错: {e}") - wcf.send_text(f"❌获取新闻出错: {str(e)}", - (roomid if roomid else sender), sender) + self.message_util.send_text(f"❌获取新闻出错: {str(e)}", + (roomid if roomid else sender), sender) finally: # 清理任务 if task_id in self._news_tasks: @@ -163,23 +162,23 @@ class GlobalNewsPlugin(MessagePluginInterface): self._run_in_executor(fox), self._run_in_executor(bbc) ] - + # 并行执行所有任务 results = await asyncio.gather(*tasks) - + # 合并结果 news_titles = "\n".join(results) - + # 使用AI分析新闻 markdown_news = await self._run_in_executor( dify_news_title_analyze, news_titles ) - + # 转换为图片 image_path = await self._run_in_executor( convert_md_str_to_image, markdown_news, "news_output.png" ) - + return image_path except Exception as e: self.LOG.error(f"异步获取新闻失败: {e}") @@ -188,4 +187,4 @@ class GlobalNewsPlugin(MessagePluginInterface): async def _run_in_executor(self, func, *args): """在线程池中运行同步函数""" loop = asyncio.get_event_loop() - return await loop.run_in_executor(None, func, *args) \ No newline at end of file + return await loop.run_in_executor(None, func, *args) diff --git a/plugins/group_add/main.py b/plugins/group_add/main.py index 90235fb..b599cd5 100644 --- a/plugins/group_add/main.py +++ b/plugins/group_add/main.py @@ -3,7 +3,6 @@ import re from datetime import datetime from typing import Dict, Any, List, Optional, Tuple -from wcferry import Wcf from plugin_common.message_plugin_interface import MessagePluginInterface from plugin_common.plugin_interface import PluginStatus @@ -45,8 +44,6 @@ class GroupAddPlugin(MessagePluginInterface): self.LOG = logging.getLogger(f"Plugin.{self.name}") self.LOG.info(f"正在初始化 {self.name} 插件...") - # 保存上下文对象 - self.wcf = context.get("wcf") # 获取群管理器 self.gbm = context.get("gbm") @@ -99,8 +96,7 @@ class GroupAddPlugin(MessagePluginInterface): """处理消息""" content = message.get("content", "") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") - + self.LOG.info(f"插件执行: {self.name}:{content}") # 提取昵称 @@ -116,8 +112,8 @@ class GroupAddPlugin(MessagePluginInterface): welcome_message = f"🎉 欢迎 【{nickname}】 加入群聊👋 \n 🕒 {now_time} 🕒 !" # 发送欢迎消息 - wcf.send_text(welcome_message, roomid) - + # self.me.send_text(welcome_message, roomid) + self.LOG.info(f"已发送欢迎消息给新成员 {nickname} 在群 {roomid}") return True, f"已欢迎 {nickname}" diff --git a/plugins/group_auto_invite/main.py b/plugins/group_auto_invite/main.py index 66b945d..ece3a9d 100644 --- a/plugins/group_auto_invite/main.py +++ b/plugins/group_auto_invite/main.py @@ -3,8 +3,9 @@ import redis import re from typing import Dict, Any, List, Optional, Tuple -from wcferry import Wcf +from gewechat_client import GewechatClient +from message_util import MessageUtil from plugin_common.message_plugin_interface import MessagePluginInterface from plugin_common.plugin_interface import PluginStatus from utils.decorator.plugin_decorators import plugin_stats_decorator @@ -50,13 +51,13 @@ class GroupAutoInvitePlugin(MessagePluginInterface): self.LOG = logging.getLogger(f"Plugin.{self.name}") self.LOG.info(f"正在初始化 {self.name} 插件...") - # 保存上下文对象 - self.wcf = context.get("wcf") # 获取群管理器 self.gbm = context.get("gbm") # 获取Redis连接池 self.redis_pool = context.get("redis_pool") + self.clent:GewechatClient = context.get("clent") + self.message_util: MessageUtil = context.get("message_util") # 从配置中获取命令和启用状态 plugin_config = self._config.get("GroupAutoInvite", {}) self._commands = plugin_config.get("command", ["#加群配置"]) @@ -111,27 +112,26 @@ class GroupAutoInvitePlugin(MessagePluginInterface): sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") gbm: GroupBotManager = message.get("gbm") # 处理加群配置命令 if content.startswith("#加群配置|"): - return self._handle_config_command(content, sender, roomid, wcf, gbm) + return self._handle_config_command(content, sender, roomid, gbm) # 处理加群请求 match = re.search(r"^#加群\s+(\w+)$", content) if match: - return self._handle_join_request(match.group(1), sender, roomid, wcf, gbm) + return self._handle_join_request(match.group(1), sender, roomid, gbm) return False, "无法处理的消息" - def _handle_config_command(self, content: str, sender: str, roomid: str, wcf: Wcf, gbm: GroupBotManager) -> Tuple[ + def _handle_config_command(self, content: str, sender: str, roomid: str, gbm: GroupBotManager) -> Tuple[ bool, Optional[str]]: """处理配置命令""" # 检查是否为管理员 admin_list = self.gbm.get_admin_list() if sender not in admin_list: - wcf.send_text("⚠️ 权限不足,只有管理员才能配置群邀请功能", + self.message_util.send_text("⚠️ 权限不足,只有管理员才能配置群邀请功能", (roomid if roomid else sender), sender) return True, "权限不足" @@ -140,10 +140,10 @@ class GroupAutoInvitePlugin(MessagePluginInterface): result = self.process_command(command) # 发送结果 - wcf.send_text(result, (roomid if roomid else sender), sender) + self.message_util.send_text(result, (roomid if roomid else sender), sender) return True, "配置命令处理成功" - def _handle_join_request(self, key: str, sender: str, roomid: str, wcf: Wcf, gbm: GroupBotManager) -> Tuple[ + def _handle_join_request(self, key: str, sender: str, roomid: str, gbm: GroupBotManager) -> Tuple[ bool, Optional[str]]: """处理加群请求""" try: @@ -152,29 +152,29 @@ class GroupAutoInvitePlugin(MessagePluginInterface): # 检查是否找到群ID if isinstance(group_id, str) and "没有关联的群ID" in group_id: - wcf.send_text(f"⚠️ 未找到关键词 '{key}' 对应的群聊", sender) + self.message_util.send_text(f"⚠️ 未找到关键词 '{key}' 对应的群聊", sender) return True, "未找到群聊" # 判断是否在群里面,如果在,则不添加 con = ContactManager.get_instance() members = con.get_group_members(group_id) # 如果在群里面,则不添加 if sender in members: - wcf.send_text(f"⚠️ 你已经在群聊中了,无需重复添加", sender) + self.message_util.send_text(f"⚠️ 你已经在群聊中了,无需重复添加", sender) return True, "你已经在群聊中了" # 发送邀请 self.LOG.info(f"邀请用户 {sender} 加入群 {group_id}") - result = wcf.invite_chatroom_members(group_id, sender) + result = self.message_util.invite_member(group_id, sender) if result: - wcf.send_text(f"✅ 已发送邀请,请查看群聊邀请通知", sender) + self.message_util.send_text(f"✅ 已发送邀请,请查看群聊邀请通知", sender) return True, "邀请发送成功" else: - wcf.send_text(f"❌ 邀请发送失败,请稍后再试", sender) + self.message_util.send_text(f"❌ 邀请发送失败,请稍后再试", sender) return False, "邀请发送失败" except Exception as e: self.LOG.error(f"处理加群请求出错: {e}") - wcf.send_text(f"❌ 处理加群请求出错: {e}", sender) + self.message_util.send_text(f"❌ 处理加群请求出错: {e}", sender) return False, f"处理出错: {e}" def add_mapping(self, key, group_id): diff --git a/plugins/group_member_change/main.py b/plugins/group_member_change/main.py index df1e03c..a75ceaa 100644 --- a/plugins/group_member_change/main.py +++ b/plugins/group_member_change/main.py @@ -4,8 +4,6 @@ import time from datetime import datetime from typing import Dict, Any, List, Optional, Tuple -from wcferry import Wcf - from plugin_common.message_plugin_interface import MessagePluginInterface from plugin_common.plugin_interface import PluginStatus from utils.robot_cmd.robot_command import Feature, PermissionStatus, GroupBotManager @@ -51,8 +49,6 @@ class GroupMemberChangePlugin(MessagePluginInterface): """初始化插件""" self.LOG.info(f"正在初始化 {self.name} 插件...") - # 保存上下文对象 - self.wcf: Wcf = context.get("wcf") # 创建消息工具实例message_util self.message_util: MessageUtil = context.get("message_util") @@ -130,7 +126,7 @@ class GroupMemberChangePlugin(MessagePluginInterface): """检查指定群的成员变化""" try: # 获取当前群成员 - current_members = self.wcf.get_chatroom_members(group_id) + current_members = self.message_util.get_chatroom_members(group_id) # 添加安全检查:如果获取到的成员列表为空,可能是接口异常 if not current_members: diff --git a/plugins/group_virtual/main.py b/plugins/group_virtual/main.py index bf3b2e7..fd83314 100644 --- a/plugins/group_virtual/main.py +++ b/plugins/group_virtual/main.py @@ -1,8 +1,7 @@ import logging from typing import Dict, Any, List, Optional, Tuple -from wcferry import Wcf - +from gewechat.call_back_message.message import WxMessage from message_util import MessageUtil from plugin_common.message_plugin_interface import MessagePluginInterface from plugin_common.plugin_interface import PluginStatus @@ -51,7 +50,6 @@ class GroupVirtualPlugin(MessagePluginInterface): self.LOG.info(f"正在初始化 {self.name} 插件...") # 保存上下文对象 - self.wcf: Wcf = context.get("wcf") self.event_system = context.get("event_system") self.message_util: MessageUtil = context.get("message_util") @@ -99,9 +97,9 @@ class GroupVirtualPlugin(MessagePluginInterface): """处理消息""" roomid = message.get("roomid", "") sender = message.get("sender", "") - + full_wx_msg: WxMessage = message.get("full_wx_msg", "") # 检查是否是机器人自己发送的消息 - if sender == self.wcf.self_wxid: + if full_wx_msg.from_self(): return False, "不转发自己的消息" # 获取该群所在的所有虚拟聊天组 diff --git a/plugins/message_recall/main.py b/plugins/message_recall/main.py index b042524..9ad9e07 100644 --- a/plugins/message_recall/main.py +++ b/plugins/message_recall/main.py @@ -6,7 +6,6 @@ from plugin_common.message_plugin_interface import MessagePluginInterface from plugin_common.plugin_interface import PluginStatus from utils.decorator.plugin_decorators import plugin_stats_decorator from utils.robot_cmd.robot_command import GroupBotManager -from wcferry import Wcf class MessageRecallPlugin(MessagePluginInterface): @@ -44,8 +43,6 @@ class MessageRecallPlugin(MessagePluginInterface): self.LOG = logging.getLogger(f"Plugin.{self.name}") self.LOG.info(f"正在初始化 {self.name} 插件...") - # 保存上下文对象 - self.wcf: Wcf = context.get("wcf") self.event_system = context.get("event_system") self.message_util: MessageUtil = context.get("message_util") @@ -92,48 +89,47 @@ class MessageRecallPlugin(MessagePluginInterface): sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") # 检查是否是管理员 admin_list = GroupBotManager.get_admin_list() self.LOG.info(f"admin_list={admin_list}") # if sender not in admin_list: - # wcf.send_text("⚠️ 权限不足,只有管理员才能撤回消息", + # self.message_util.send_text("⚠️ 权限不足,只有管理员才能撤回消息", # (roomid if roomid else sender), sender) # return True, "权限不足" # 解析命令获取消息ID parts = content.split(" ", 1) if len(parts) < 2: - wcf.send_text(f"❌命令格式错误!\n{self.command_format}", + self.message_util.send_text(f"❌命令格式错误!\n{self.command_format}", (roomid if roomid else sender), sender) return True, "命令格式错误" - - try: - # 从数据库里面提取可以处理的消息and StrTalker ={roomid} - sql = (f"SELECT * FROM MSG where IsSender=1 and CreateTime > (strftime('%s', 'now') - 120) " - f"limit {parts[1]}") - data = wcf.query_sql('MSG0.db', sql) - self.LOG.info(f"SQL:{sql}\n 查询到可撤回数据: {data}") - if not data: - wcf.send_text("❌ 没有可撤回的消息", (roomid if roomid else sender), sender) - return True, "没有可撤回的消息" - for item in data: - if item["MsgSvrID"]: - # 调用撤回消息API - result = wcf.revoke_msg(item["MsgSvrID"]) - if result: - wcf.send_text("✅ 消息撤回成功", (roomid if roomid else sender), sender) - return True, "撤回成功" - else: - wcf.send_text("❌ 消息撤回失败,可能是消息ID无效或已超过撤回时间限制(2分钟)", - (roomid if roomid else sender), sender) - return True, "撤回失败" - - except Exception as e: - self.LOG.error(f"撤回消息出错: {e}") - wcf.send_text(f"❌ 撤回消息出错: {str(e)}", (roomid if roomid else sender), sender) - return True, f"处理出错: {e}" + # + # try: + # # 从数据库里面提取可以处理的消息and StrTalker ={roomid} + # sql = (f"SELECT * FROM MSG where IsSender=1 and CreateTime > (strftime('%s', 'now') - 120) " + # f"limit {parts[1]}") + # data = self.message_util.query_sql('MSG0.db', sql) + # self.LOG.info(f"SQL:{sql}\n 查询到可撤回数据: {data}") + # if not data: + # self.message_util.send_text("❌ 没有可撤回的消息", (roomid if roomid else sender), sender) + # return True, "没有可撤回的消息" + # for item in data: + # if item["MsgSvrID"]: + # # 调用撤回消息API + # result = self.message_util.revoke_msg(item["MsgSvrID"]) + # if result: + # self.message_util.send_text("✅ 消息撤回成功", (roomid if roomid else sender), sender) + # return True, "撤回成功" + # else: + # self.message_util.send_text("❌ 消息撤回失败,可能是消息ID无效或已超过撤回时间限制(2分钟)", + # (roomid if roomid else sender), sender) + # return True, "撤回失败" + # + # except Exception as e: + # self.LOG.error(f"撤回消息出错: {e}") + # self.message_util.send_text(f"❌ 撤回消息出错: {str(e)}", (roomid if roomid else sender), sender) + # return True, f"处理出错: {e}" def get_help(self) -> str: """获取插件帮助信息""" diff --git a/plugins/message_sign/main.py b/plugins/message_sign/main.py index 01b818c..e8031f0 100644 --- a/plugins/message_sign/main.py +++ b/plugins/message_sign/main.py @@ -3,7 +3,6 @@ import logging import pytz from typing import Dict, Any, List, Optional, Tuple -from wcferry import Wcf from db.connection import DBConnectionManager from message_util import MessageUtil @@ -64,7 +63,6 @@ class MessageSignPlugin(MessagePluginInterface): self.LOG.info(f"正在初始化 {self.name} 插件...") # 保存上下文对象 - self.wcf = context.get("wcf") self.event_system = context.get("event_system") self.message_util: MessageUtil = context.get("message_util") self.gbm = context.get("gbm") @@ -217,7 +215,6 @@ class MessageSignPlugin(MessagePluginInterface): """处理签到请求""" sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") all_contacts = message.get("all_contacts", {}) try: diff --git a/plugins/message_summary/main.py b/plugins/message_summary/main.py index 2db8c03..3119881 100644 --- a/plugins/message_summary/main.py +++ b/plugins/message_summary/main.py @@ -5,6 +5,7 @@ from typing import Dict, Any, Tuple, Optional, List import requests +from message_util import MessageUtil from utils.string_utils import remove_trailing_content from utils.wechat.message_to_db import MessageStorage from utils.compress_chat_data import compress_chat_data @@ -54,6 +55,8 @@ class MessageSummaryPlugin(MessagePluginInterface): self._api_key = api_config.get("api_key", "app-McGLzBhBjeBCSEi7n83MtuTo") self._api_url = api_config.get("api_url", "http://192.168.2.240/v1/chat-messages") self.message_storage = MessageStorage() + self.message_util: MessageUtil = context.get("message_util") + self.LOG.info(f"初始化 {self.name} 插件成功") return True except Exception as e: @@ -88,13 +91,10 @@ class MessageSummaryPlugin(MessagePluginInterface): command = content[len(self.command_prefix):].split()[0] if command not in self.commands: return False, None - wcf = message.get("wcf") # 获取需要总结的内容 group_id = message.get("roomid") if not group_id: - # 直接发送消息 - if wcf: - wcf.send_text("只支持群聊消息总结", message.get("sender")) + self.message_util.send_text("只支持群聊消息总结", message.get("sender")) return False, None # 权限判断 gbm: GroupBotManager = message.get("gbm") @@ -110,18 +110,17 @@ class MessageSummaryPlugin(MessagePluginInterface): # 获取群名并处理 group_name = all_contacts.get(group_id, group_id) group_name = self._sanitize_group_name(group_name) - if wcf: - wcf.send_text("⏳群消息总结中… 😊", group_id) + self.message_util.send_text("⏳群消息总结中… 😊", group_id) - # 创建线程异步处理总结生成和发送 - summary_thread = threading.Thread( - target=self._async_generate_and_send_summary, - args=(chat_content, group_name, group_id, message) - ) - summary_thread.daemon = True # 设置为守护线程,主程序退出时线程也会退出 - summary_thread.start() + # 创建线程异步处理总结生成和发送 + summary_thread = threading.Thread( + target=self._async_generate_and_send_summary, + args=(chat_content, group_name, group_id, message) + ) + summary_thread.daemon = True # 设置为守护线程,主程序退出时线程也会退出 + summary_thread.start() - return True, "异步总结已启动" + return True, "异步总结已启动" except Exception as e: self.LOG.error(f"处理消息总结命令失败: {e}") @@ -138,17 +137,17 @@ class MessageSummaryPlugin(MessagePluginInterface): wcf = message.get("wcf") if wcf: # if summary: - # wcf.send_text(f"总结已生成:\n{summary}", group_id, message.get("sender")) + # self.message_util.send_text(f"总结已生成:\n{summary}", group_id, message.get("sender")) if image_path: - wcf.send_file(image_path, group_id) + self.message_util.send_file(image_path, group_id) else: - wcf.send_text("❌ 生成总结图片失败", group_id) + self.message_util.send_text("❌ 生成总结图片失败", group_id) except Exception as e: self.LOG.error(f"异步生成总结失败: {e}") wcf = message.get("wcf") if wcf: - wcf.send_text(f"❌ 生成总结失败: {str(e)}", group_id) + self.message_util.send_text(f"❌ 生成总结失败: {str(e)}", group_id) def _sanitize_group_name(self, group_name: str) -> str: """处理群名,去除特殊字符并限制长度""" diff --git a/plugins/music/main.py b/plugins/music/main.py index 0a14134..dbcaf5e 100644 --- a/plugins/music/main.py +++ b/plugins/music/main.py @@ -3,7 +3,6 @@ import requests import lz4.block as lb from typing import Dict, Any, List, Optional, Tuple -from wcferry import Wcf from plugin_common.message_plugin_interface import MessagePluginInterface from plugin_common.plugin_interface import PluginStatus @@ -48,7 +47,6 @@ class MusicPlugin(MessagePluginInterface): self.LOG.info(f"正在初始化 {self.name} 插件...") # 保存上下文对象 - self.wcf = context.get("wcf") self.event_system = context.get("event_system") self.message_util = context.get("message_util") @@ -90,12 +88,11 @@ class MusicPlugin(MessagePluginInterface): command = content.split(" ")[0] sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") gbm: GroupBotManager = message.get("gbm") # 检查命令格式 if len(content.split(" ")) == 1: - wcf.send_text(f"❌命令格式错误!\n{self.command_format}", + self.message_util.send_text(f"❌命令格式错误!\n{self.command_format}", (roomid if roomid else sender), sender) return False, "命令格式错误" @@ -110,7 +107,7 @@ class MusicPlugin(MessagePluginInterface): # 搜索歌曲 song_info = self._search_song(user_song_name) if not song_info or not song_info.get("play_url"): - wcf.send_text(f"❌未找到歌曲:{user_song_name}", + self.message_util.send_text(f"❌未找到歌曲:{user_song_name}", (roomid if roomid else sender), sender) return False, "未找到歌曲" @@ -204,19 +201,18 @@ class MusicPlugin(MessagePluginInterface): """ - - # 修改消息数据库里面的消息content内容 - text_bytes = xml_message.encode('utf-8') - compressed_data = lb.compress(text_bytes, store_size=False).hex() - - data = wcf.query_sql('MSG0.db', "SELECT * FROM MSG where type = 49 limit 1") - wcf.query_sql('MSG0.db', - f"""UPDATE MSG SET CompressContent = x'{compressed_data}', BytesExtra=x'', type=49, SubType=3, - IsSender=0, TalkerId=2 WHERE MsgSvrID={data[0]['MsgSvrID']}""" - ) - - result = wcf.forward_msg(data[0]["MsgSvrID"], receiver) - self.LOG.info(f"插件化:点歌发送结果: {result}") + # # 修改消息数据库里面的消息content内容 + # text_bytes = xml_message.encode('utf-8') + # compressed_data = lb.compress(text_bytes, store_size=False).hex() + # + # data = self.message_util.query_sql('MSG0.db', "SELECT * FROM MSG where type = 49 limit 1") + # self.message_util.query_sql('MSG0.db', + # f"""UPDATE MSG SET CompressContent = x'{compressed_data}', BytesExtra=x'', type=49, SubType=3, + # IsSender=0, TalkerId=2 WHERE MsgSvrID={data[0]['MsgSvrID']}""" + # ) + # + # result = self.message_util.forward_msg(data[0]["MsgSvrID"], receiver) + # self.LOG.info(f"插件化:点歌发送结果: {result}") return True except Exception as e: diff --git a/plugins/plugin_manager/main.py b/plugins/plugin_manager/main.py index 360ed46..206fd97 100644 --- a/plugins/plugin_manager/main.py +++ b/plugins/plugin_manager/main.py @@ -94,14 +94,14 @@ class PluginManagerPlugin(MessagePluginInterface): # 检查命令格式 parts = content.split(" ") if len(parts) == 1: - wcf.send_text(f"❌命令格式错误!\n{self.command_format}", target, sender) + self.message_util.send_text(f"❌命令格式错误!\n{self.command_format}", target, sender) return True, "命令格式错误" # 只有使用的时候才全局获取对象。防止在预加载的时候跟主线程冲突 self.plugin_registry = PluginRegistry() self.plugin_manager = PluginManager().get_instance() # 检查权限 (只允许管理员操作) if not self._is_admin(sender, gbm): - wcf.send_text(f"❌权限不足,只有管理员可以管理插件", target, sender) + self.message_util.send_text(f"❌权限不足,只有管理员可以管理插件", target, sender) return True, "权限不足" # 解析子命令 @@ -125,14 +125,14 @@ class PluginManagerPlugin(MessagePluginInterface): if handler and (sub_command == "列表" or plugin_name): return handler(wcf, sender, roomid) else: - wcf.send_text(f"❌未知命令或缺少参数!\n{self.command_format}", target, sender) + self.message_util.send_text(f"❌未知命令或缺少参数!\n{self.command_format}", target, sender) return True, "未知命令" except Exception as e: import traceback error_trace = traceback.format_exc() self.LOG.error(f"处理插件管理请求出错: {e}\n{error_trace}") - wcf.send_text(f"❌操作失败: {str(e)}", target, sender) + self.message_util.send_text(f"❌操作失败: {str(e)}", target, sender) return True, f"处理出错: {e}" def _is_admin(self, user_id: str, gbm: GroupBotManager) -> bool: @@ -153,7 +153,7 @@ class PluginManagerPlugin(MessagePluginInterface): module_name = plugin.__class__.__module__.split('.')[-2] message += f"{status}-{plugin.name} [模块: {module_name}]\n" - wcf.send_text(message, target, sender) + self.message_util.send_text(message, target, sender) return True, "列出插件成功" def _operate_plugin(self, plugin_name: str, wcf, sender: str, roomid: str, @@ -165,12 +165,12 @@ class PluginManagerPlugin(MessagePluginInterface): display_name, plugin = self.plugin_manager.find_plugin_by_name(plugin_name) if not display_name: - wcf.send_text(f"❌未找到插件 {plugin_name},请检查名称是否正确", target, sender) + self.message_util.send_text(f"❌未找到插件 {plugin_name},请检查名称是否正确", target, sender) return True, f"未找到插件 {plugin_name}" # 不允许操作自身(对于某些操作) if display_name == self.name and operation_func in [self._unload_plugin, self._disable_plugin]: - wcf.send_text(f"⚠️不能对插件管理插件自身执行此操作", target, sender) + self.message_util.send_text(f"⚠️不能对插件管理插件自身执行此操作", target, sender) return True, "不能对插件管理插件自身执行此操作" # 执行具体操作 @@ -182,11 +182,11 @@ class PluginManagerPlugin(MessagePluginInterface): plugin = self.plugin_registry.get_plugin(plugin_name) if not plugin: - wcf.send_text(f"❌插件 {plugin_name} 不存在", target, sender) + self.message_util.send_text(f"❌插件 {plugin_name} 不存在", target, sender) return True, f"插件 {plugin_name} 不存在" if plugin.status == PluginStatus.RUNNING: - wcf.send_text(f"⚠️插件 {plugin_name} 已经处于启用状态", target, sender) + self.message_util.send_text(f"⚠️插件 {plugin_name} 已经处于启用状态", target, sender) return True, f"插件 {plugin_name} 已经处于启用状态" # 获取插件的模块名 @@ -194,10 +194,10 @@ class PluginManagerPlugin(MessagePluginInterface): # 启动插件 if self.plugin_manager.start_plugin(module_name): - wcf.send_text(f"✅插件 {plugin_name} 启用成功", target, sender) + self.message_util.send_text(f"✅插件 {plugin_name} 启用成功", target, sender) return True, f"插件 {plugin_name} 启用成功" else: - wcf.send_text(f"❌插件 {plugin_name} 启用失败", target, sender) + self.message_util.send_text(f"❌插件 {plugin_name} 启用失败", target, sender) return False, f"插件 {plugin_name} 启用失败" def _disable_plugin(self, plugin_name: str, wcf, sender: str, roomid: str) -> Tuple[bool, str]: @@ -206,11 +206,11 @@ class PluginManagerPlugin(MessagePluginInterface): plugin = self.plugin_registry.get_plugin(plugin_name) if not plugin: - wcf.send_text(f"❌插件 {plugin_name} 不存在", target, sender) + self.message_util.send_text(f"❌插件 {plugin_name} 不存在", target, sender) return True, f"插件 {plugin_name} 不存在" if plugin.status != PluginStatus.RUNNING: - wcf.send_text(f"⚠️插件 {plugin_name} 已经处于禁用状态", target, sender) + self.message_util.send_text(f"⚠️插件 {plugin_name} 已经处于禁用状态", target, sender) return True, f"插件 {plugin_name} 已经处于禁用状态" # 获取插件的模块名 @@ -218,10 +218,10 @@ class PluginManagerPlugin(MessagePluginInterface): # 停止插件 if self.plugin_manager.stop_plugin(module_name): - wcf.send_text(f"✅插件 {plugin_name} 禁用成功", target, sender) + self.message_util.send_text(f"✅插件 {plugin_name} 禁用成功", target, sender) return True, f"插件 {plugin_name} 禁用成功" else: - wcf.send_text(f"❌插件 {plugin_name} 禁用失败", target, sender) + self.message_util.send_text(f"❌插件 {plugin_name} 禁用失败", target, sender) return False, f"插件 {plugin_name} 禁用失败" def _reload_plugin(self, plugin_name: str, wcf, sender: str, roomid: str) -> Tuple[bool, str]: @@ -230,7 +230,7 @@ class PluginManagerPlugin(MessagePluginInterface): plugin = self.plugin_registry.get_plugin(plugin_name) if not plugin: - wcf.send_text(f"❌插件 {plugin_name} 不存在", target, sender) + self.message_util.send_text(f"❌插件 {plugin_name} 不存在", target, sender) return True, f"插件 {plugin_name} 不存在" # 记录原插件状态 @@ -243,10 +243,10 @@ class PluginManagerPlugin(MessagePluginInterface): reloaded_plugin = self.plugin_manager.reload_plugin(module_name) if reloaded_plugin: - wcf.send_text(f"✅插件 {plugin_name} 重载成功", target, sender) + self.message_util.send_text(f"✅插件 {plugin_name} 重载成功", target, sender) return True, f"插件 {plugin_name} 重载成功" else: - wcf.send_text(f"❌插件 {plugin_name} 重载失败", target, sender) + self.message_util.send_text(f"❌插件 {plugin_name} 重载失败", target, sender) return False, f"插件 {plugin_name} 重载失败" def _unload_plugin(self, plugin_name: str, wcf, sender: str, roomid: str) -> Tuple[bool, str]: @@ -255,7 +255,7 @@ class PluginManagerPlugin(MessagePluginInterface): plugin = self.plugin_registry.get_plugin(plugin_name) if not plugin: - wcf.send_text(f"❌插件 {plugin_name} 不存在", target, sender) + self.message_util.send_text(f"❌插件 {plugin_name} 不存在", target, sender) return True, f"插件 {plugin_name} 不存在" # 获取插件的模块名 @@ -263,10 +263,10 @@ class PluginManagerPlugin(MessagePluginInterface): # 卸载插件 if self.plugin_manager.unload_plugin(module_name): - wcf.send_text(f"✅插件 {plugin_name} 卸载成功", target, sender) + self.message_util.send_text(f"✅插件 {plugin_name} 卸载成功", target, sender) return True, f"插件 {plugin_name} 卸载成功" else: - wcf.send_text(f"❌插件 {plugin_name} 卸载失败", target, sender) + self.message_util.send_text(f"❌插件 {plugin_name} 卸载失败", target, sender) return False, f"插件 {plugin_name} 卸载失败" def _load_plugin(self, plugin_name: str, wcf, sender: str, roomid: str, silent: bool = False) -> Tuple[bool, str]: @@ -276,7 +276,7 @@ class PluginManagerPlugin(MessagePluginInterface): plugin_dir = os.path.join("plugins", plugin_name) if not os.path.exists(plugin_dir): if not silent: - wcf.send_text(f"❌插件目录 {plugin_dir} 不存在", + self.message_util.send_text(f"❌插件目录 {plugin_dir} 不存在", (roomid if roomid else sender), sender) return False, f"插件目录 {plugin_dir} 不存在" @@ -285,7 +285,7 @@ class PluginManagerPlugin(MessagePluginInterface): existing_module_name = existing_plugin.__class__.__module__.split('.')[-2] if existing_module_name == plugin_name: if not silent: - wcf.send_text(f"⚠️插件 {existing_plugin.name} (模块名: {plugin_name}) 已经加载", + self.message_util.send_text(f"⚠️插件 {existing_plugin.name} (模块名: {plugin_name}) 已经加载", (roomid if roomid else sender), sender) return True, f"插件 {existing_plugin.name} 已经加载" @@ -294,18 +294,18 @@ class PluginManagerPlugin(MessagePluginInterface): plugin = self.plugin_manager.load_plugin(plugin_name) if plugin: if not silent: - wcf.send_text(f"✅插件 {plugin.name} 加载成功", + self.message_util.send_text(f"✅插件 {plugin.name} 加载成功", (roomid if roomid else sender), sender) return True, f"插件 {plugin.name} 加载成功" else: if not silent: - wcf.send_text(f"❌插件 {plugin_name} 加载失败", + self.message_util.send_text(f"❌插件 {plugin_name} 加载失败", (roomid if roomid else sender), sender) return False, f"插件 {plugin_name} 加载失败" except Exception as e: self.LOG.error(f"加载插件 {plugin_name} 出错: {e}") if not silent: - wcf.send_text(f"❌加载插件出错: {str(e)}", + self.message_util.send_text(f"❌加载插件出错: {str(e)}", (roomid if roomid else sender), sender) return False, f"加载插件出错: {e}" @@ -315,13 +315,13 @@ class PluginManagerPlugin(MessagePluginInterface): display_name, plugin = self.plugin_manager.find_plugin_by_name(plugin_name) if not display_name: - wcf.send_text(f"❌未找到插件 {plugin_name},请检查名称是否正确", + self.message_util.send_text(f"❌未找到插件 {plugin_name},请检查名称是否正确", (roomid if roomid else sender), sender) return True, f"未找到插件 {plugin_name}" plugin = self.plugin_registry.get_plugin(display_name) if not plugin: - wcf.send_text(f"❌插件 {display_name} 不存在", + self.message_util.send_text(f"❌插件 {display_name} 不存在", (roomid if roomid else sender), sender) return True, f"插件 {display_name} 不存在" @@ -340,5 +340,5 @@ class PluginManagerPlugin(MessagePluginInterface): 🔑 命令:{', '.join(plugin.commands) if hasattr(plugin, 'commands') else '无'} """ - wcf.send_text(message, (roomid if roomid else sender), sender) + self.message_util.send_text(message, (roomid if roomid else sender), sender) return True, "查看插件详情成功" diff --git a/plugins/point_trade/main.py b/plugins/point_trade/main.py index ae566ad..e97354a 100644 --- a/plugins/point_trade/main.py +++ b/plugins/point_trade/main.py @@ -4,7 +4,6 @@ from datetime import datetime from typing import Dict, Any, List, Optional, Tuple import xml.etree.ElementTree as ET -from wcferry import Wcf from db.connection import DBConnectionManager from db.points_db import PointsDBOperator, PointSource @@ -54,7 +53,6 @@ class PointTradePlugin(MessagePluginInterface): self.LOG.info(f"正在初始化 {self.name} 插件...") # 保存上下文对象 - self.wcf = context.get("wcf") self.event_system = context.get("event_system") self.message_util: MessageUtil = context.get("message_util") self.gbm = context.get("gbm") @@ -129,7 +127,6 @@ class PointTradePlugin(MessagePluginInterface): command = content.split(" ") sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") gbm: GroupBotManager = message.get("gbm") xml = message.get("xml", "") @@ -159,7 +156,6 @@ class PointTradePlugin(MessagePluginInterface): command = content.split(" ") sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") xml = message.get("xml", "") # 检查命令格式 @@ -312,7 +308,6 @@ class PointTradePlugin(MessagePluginInterface): """处理积分排行榜命令""" sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") if not roomid: self.message_util.send_text("❌积分排行榜仅在群聊中可用!", sender, "") @@ -452,7 +447,6 @@ class PointTradePlugin(MessagePluginInterface): content = str(message.get("content", "")).strip() sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") xml = message.get("xml", "") # 检查是否在群聊中 @@ -633,7 +627,6 @@ class PointTradePlugin(MessagePluginInterface): """处理保释命令""" sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") xml = message.get("xml", "") # 检查是否在群聊中 diff --git a/plugins/system_updater/main.py b/plugins/system_updater/main.py index 417f2b7..654d531 100644 --- a/plugins/system_updater/main.py +++ b/plugins/system_updater/main.py @@ -78,7 +78,6 @@ class SystemUpdaterPlugin(MessagePluginInterface): self.LOG.info(f"正在初始化 {self.name} 插件...") # 保存上下文对象 - self.wcf = context.get("wcf") self.message_util = context.get("message_util") self.config = self._config @@ -127,12 +126,11 @@ class SystemUpdaterPlugin(MessagePluginInterface): content = str(message.get("content", "")).strip() sender = message.get("sender") roomid = message.get("roomid", "") - wcf = message.get("wcf") gbm = message.get("gbm", None) # 检查权限 if self.admin_wxids and sender not in self.admin_wxids: - wcf.send_text("⚠️ 您没有执行此操作的权限", + self.message_util.send_text("⚠️ 您没有执行此操作的权限", (roomid if roomid else sender), sender) return True, "无权限" @@ -156,12 +154,12 @@ class SystemUpdaterPlugin(MessagePluginInterface): # 检查win_click模块是否可用 if win_click is None: - wcf.send_text("⚠️ 无法执行更新操作,系统缺少必要的组件", + self.message_util.send_text("⚠️ 无法执行更新操作,系统缺少必要的组件", (roomid if roomid else sender), sender) return True, "缺少win_click模块" # 发送更新通知 - wcf.send_text(f"🔄 系统即将更新并重启,等待时间设置为{wait_time}秒...", + self.message_util.send_text(f"🔄 系统即将更新并重启,等待时间设置为{wait_time}秒...", (roomid if roomid else sender), sender) # 启动更新流程 diff --git a/plugins/video/main.py b/plugins/video/main.py index cca2dd2..9df3bc6 100644 --- a/plugins/video/main.py +++ b/plugins/video/main.py @@ -3,7 +3,6 @@ import os import requests from typing import Dict, Any, List, Optional, Tuple -from wcferry import Wcf from plugin_common.message_plugin_interface import MessagePluginInterface from plugin_common.plugin_interface import PluginStatus @@ -50,7 +49,6 @@ class VideoPlugin(MessagePluginInterface): self.LOG.info(f"正在初始化 {self.name} 插件...") # 保存上下文对象 - self.wcf = context.get("wcf") self.event_system = context.get("event_system") self.message_util = context.get("message_util") self.gbm = context.get("gbm") @@ -96,7 +94,6 @@ class VideoPlugin(MessagePluginInterface): self.LOG.info(f"插件执行: {self.name}:{content}") sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") gbm: GroupBotManager = message.get("gbm") # 检查权限 @@ -109,18 +106,18 @@ class VideoPlugin(MessagePluginInterface): file_abspath = self._download_stream("https://api.guiguiya.com/api/hook/heisis", save_path) if not file_abspath or not file_abspath.endswith("mp4"): - wcf.send_text(f"\n❌视频下载失败,请稍后再试", + self.message_util.send_text(f"\n❌视频下载失败,请稍后再试", (roomid if roomid else sender), sender) return False, "视频下载失败" # 发送视频 - result = wcf.send_file(file_abspath, (roomid if roomid else sender)) + result = self.message_util.send_file(file_abspath, (roomid if roomid else sender)) self.LOG.info(f"发送视频结果: {result}") return True, "发送成功" except Exception as e: self.LOG.error(f"处理视频请求出错: {e}") - wcf.send_text(f"\n❌请求出错:{e}", + self.message_util.send_text(f"\n❌请求出错:{e}", (roomid if roomid else sender), sender) return False, f"处理出错: {e}" diff --git a/plugins/video_man/main.py b/plugins/video_man/main.py index 85752bf..4edd954 100644 --- a/plugins/video_man/main.py +++ b/plugins/video_man/main.py @@ -3,8 +3,6 @@ import os import requests from typing import Dict, Any, List, Optional, Tuple -from wcferry import Wcf - from message_util import MessageUtil from plugin_common.message_plugin_interface import MessagePluginInterface from plugin_common.plugin_interface import PluginStatus @@ -51,7 +49,6 @@ class VideoManPlugin(MessagePluginInterface): self.LOG.info(f"正在初始化 {self.name} 插件...") # 保存上下文对象 - self.wcf = context.get("wcf") self.event_system = context.get("event_system") self.message_util: MessageUtil = context.get("message_util") self.gbm = context.get("gbm") @@ -97,7 +94,6 @@ class VideoManPlugin(MessagePluginInterface): self.LOG.info(f"插件执行: {self.name}:{content}") sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") gbm: GroupBotManager = message.get("gbm") # 检查权限 @@ -114,7 +110,7 @@ class VideoManPlugin(MessagePluginInterface): return False, "视频下载失败" # 发送视频 - result = wcf.send_file(file_abspath, (roomid if roomid else sender)) + result = self.message_util.send_file(file_abspath, (roomid if roomid else sender)) self.LOG.info(f"发送视频结果: {result}") return True, "发送成功" diff --git a/plugins/xiuren_image/main.py b/plugins/xiuren_image/main.py index 092ab93..a9cbe68 100644 --- a/plugins/xiuren_image/main.py +++ b/plugins/xiuren_image/main.py @@ -3,8 +3,6 @@ import os import random from typing import Dict, Any, List, Optional, Tuple -from wcferry import Wcf - from plugin_common.message_plugin_interface import MessagePluginInterface from plugin_common.plugin_interface import PluginStatus from utils.decorator.plugin_decorators import plugin_stats_decorator @@ -49,7 +47,6 @@ class XiurenImagePlugin(MessagePluginInterface): self.LOG.info(f"正在初始化 {self.name} 插件...") # 保存上下文对象 - self.wcf = context.get("wcf") self.event_system = context.get("event_system") self.message_util = context.get("message_util") @@ -95,7 +92,6 @@ class XiurenImagePlugin(MessagePluginInterface): self.LOG.info(f"插件执行: {self.name}:{content}") sender = message.get("sender") roomid = message.get("roomid", "") - wcf: Wcf = message.get("wcf") gbm: GroupBotManager = message.get("gbm") # 检查权限 @@ -106,12 +102,12 @@ class XiurenImagePlugin(MessagePluginInterface): # 获取随机图片 pic_path = self._get_random_pic() if not pic_path: - wcf.send_text(f"❌未找到图片资源", + self.message_util.send_text(f"❌未找到图片资源", (roomid if roomid else sender), sender) return False, "未找到图片资源" # 发送图片 - result = wcf.send_file(pic_path, (roomid if roomid else sender)) + result = self.message_util.send_file(pic_path, (roomid if roomid else sender)) self.LOG.info(f"发送图片结果: {result}") return True, "发送成功" diff --git a/requirements.txt b/requirements.txt index d921655..9d7d6d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,6 @@ requests~=2.32.3 schedule~=1.2.2 pyhandytools sparkdesk-api==1.3.0 -wcferry==39.5.1.0 websocket~=0.2.1 pillow~=11.0.0 jupyter_client~=8.6.3 @@ -45,4 +44,7 @@ pywin32==306 opencv-python~=4.11.0.86 deepface~=0.0.93 -scikit-learn>=1.0.2 \ No newline at end of file +scikit-learn>=1.0.2 +gewechat-client==0.1.5 +fastapi~=0.115.12 +uvicorn~=0.34.2 \ No newline at end of file diff --git a/robot.py b/robot.py index 5b3a4cd..24256d4 100644 --- a/robot.py +++ b/robot.py @@ -7,14 +7,12 @@ from queue import Empty from threading import Thread import random +from gewechat_client import GewechatClient + from base.func_doubao import Doubao from base.func_epic import is_friday, get_free from base.func_zhipu import ZhiPu -from wcferry import Wcf, WxMsg - -from base.func_bard import BardAssistant -from base.func_chatglm import ChatGLM from base.func_chatgpt import ChatGPT from base.func_news import News from base.func_tigerbot import TigerBot @@ -22,6 +20,7 @@ from base.func_xinghuo_web import XinghuoWeb from base.func_claude import Claude from configuration import Config from constants import ChatType +from gewechat.call_back_message.message import WxMessage, MessageType from utils.wechat.message_to_db import MessageStorage from plugin_common.event_system import EventType, EventSystem from plugin_common.message_plugin_interface import MessagePluginInterface @@ -33,8 +32,6 @@ from job_mgmt import Job from utils.robot_cmd.robot_command import Feature from utils.robot_cmd.robot_command import PermissionStatus -__version__ = "39.5.1.0" - from sehuatang.shehuatang import pdf_file_path from utils.wechat.contact_manager import ContactManager from xiuren.meitu_dl import meitu_dowload_pub_pic @@ -48,17 +45,16 @@ class Robot(Job): """个性化自己的机器人 """ - def __init__(self, config: Config, wcf: Wcf, chat_type: int) -> None: - self.wcf = wcf + def __init__(self, config: Config, app_id: str, client: GewechatClient, chat_type: int) -> None: + self.app_id = app_id + self.client = client self.config = config self.LOG = logging.getLogger("Robot") - self.wxid = self.wcf.get_self_wxid() # 初始化联系人管理器并设置联系人 self.contact_manager = ContactManager.get_instance() self.allContacts = self.get_all_contacts() - self.all_head_img = self.get_all_head_img_url() - self.contact_manager.set_contacts(self.allContacts, self.all_head_img, self.wcf) + self.contact_manager.set_contacts(self.allContacts) self.LOG.info(f"DB+REDIS 连接池开始初始化") # 使用单例模式获取实例 @@ -73,7 +69,7 @@ class Robot(Job): self.redis_pool = self.db_manager.redis_pool # 初始化消息工具类 - 使用联系人管理器 - self.message_util = MessageUtil(wcf, self.contact_manager) + self.message_util = MessageUtil(app_id, client, self.contact_manager) self.groups = {} # 存储按group_id分组的消息列表,每个group_id最多保留10条消息 GroupBotManager.load_local_cache() @@ -89,7 +85,7 @@ class Robot(Job): # 设置插件系统上下文 self.system_context = { "config": config, - "wcf": wcf, + "client": client, "event_system": self.event_system, "plugin_registry": self.plugin_registry, "db_pool": self.db_pool, @@ -105,48 +101,7 @@ class Robot(Job): self.LOG.info("插件系统初始化完成") # 消息存档模块初始化,自动完成入库动作 - self.message_storage = MessageStorage(self.wcf) - if ChatType.is_in_chat_types(chat_type): - if chat_type == ChatType.TIGER_BOT.value and TigerBot.value_check(self.config.TIGERBOT): - self.chat = TigerBot(self.config.TIGERBOT) - elif chat_type == ChatType.CHATGPT.value and ChatGPT.value_check(self.config.CHATGPT): - self.chat = ChatGPT(self.config.CHATGPT) - elif chat_type == ChatType.XINGHUO_WEB.value and XinghuoWeb.value_check(self.config.XINGHUO_WEB): - self.chat = XinghuoWeb(self.config.XINGHUO_WEB) - elif chat_type == ChatType.CHATGLM.value and ChatGLM.value_check(self.config.CHATGLM): - self.chat = ChatGLM(self.config.CHATGLM) - elif chat_type == ChatType.BardAssistant.value and BardAssistant.value_check(self.config.BardAssistant): - self.chat = BardAssistant(self.config.BardAssistant) - elif chat_type == ChatType.ZhiPu.value and ZhiPu.value_check(self.config.ZhiPu): - self.chat = ZhiPu(self.config.ZhiPu) - elif chat_type == ChatType.CLAUDE.value and Claude.value_check(self.config.CLAUDE): - self.chat = Claude(self.config.CLAUDE) - elif chat_type == ChatType.DOUBAO.value and Claude.value_check(self.config.DOUBAO): - self.chat = Doubao(self.config.DOUBAO) - else: - self.LOG.warning("未配置模型") - self.chat = None - else: - if TigerBot.value_check(self.config.TIGERBOT): - self.chat = TigerBot(self.config.TIGERBOT) - elif ChatGPT.value_check(self.config.CHATGPT): - self.chat = ChatGPT(self.config.CHATGPT) - elif XinghuoWeb.value_check(self.config.XINGHUO_WEB): - self.chat = XinghuoWeb(self.config.XINGHUO_WEB) - elif ChatGLM.value_check(self.config.CHATGLM): - self.chat = ChatGLM(self.config.CHATGLM) - elif BardAssistant.value_check(self.config.BardAssistant): - self.chat = BardAssistant(self.config.BardAssistant) - elif ZhiPu.value_check(self.config.ZhiPu): - self.chat = ZhiPu(self.config.ZhiPu) - elif Claude.value_check(self.config.CLAUDE): - self.chat = Claude(self.config.CLAUDE) - elif Doubao.value_check(self.config.DOUBAO): - self.chat = Doubao(self.config.DOUBAO) - else: - self.LOG.warning("未配置模型") - self.chat = None - self.LOG.info(f"已选择: {self.chat}") + self.message_storage = MessageStorage(self.client) @staticmethod def value_check(args: dict) -> bool: @@ -154,11 +109,11 @@ class Robot(Job): return all(value is not None for key, value in args.items() if key != 'proxy') return False - def toChitchat(self, msg: WxMsg) -> bool: + def toChitchat(self, msg: WxMessage) -> bool: """闲聊,接入 ChatGPT """ # 去除@的人和空格等字符 - q = re.sub(r"@.*?[\u2005|\s]", "", msg.content).replace(" ", "") + q = re.sub(r"@.*?[\u2005|\s]", "", msg.content.raw_content).replace(" ", "") if q == "#今日百度新闻": self.news_baidu_report((msg.roomid if msg.from_group() else msg.sender)) @@ -176,7 +131,7 @@ class Robot(Job): else: return True - def processMsg(self, msg: WxMsg) -> None: + def processMsg(self, msg: WxMessage) -> None: """当接收到消息的时候,会调用本方法。如果不实现本方法,则打印原始消息。 此处可进行自定义发送的内容,如通过 msg.content 关键字自动获取当前天气信息,并发送到对应的群组@发送者 群号:msg.roomid 微信ID:msg.sender 消息内容:msg.content @@ -222,7 +177,7 @@ class Robot(Job): try: self.message_storage.archive_message(msg) # 单独处理图片消息 - if msg.type == 3: # 图片消息类型 + if msg.msg_type == 3: # 图片消息类型 self.message_storage.process_image(msg) except Exception as e: self.LOG.error(f"archive_message error: {e}") @@ -237,7 +192,7 @@ class Robot(Job): rsp = self.gbm.handle_command(msg.roomid, msg.content) # 不在群里发送,防止被骚扰 if rsp is not None: - self.send_text_msg(rsp, msg.roomid, msg.sender) + self.message_util.send_text(rsp, msg.roomid, msg.sender) return except Exception as e: self.LOG.error(f"revoke_receive_message error: {e}") @@ -248,15 +203,7 @@ class Robot(Job): if plugin_processed: return - # 非群聊信息,按消息类型进行处理 - if msg.type == 37: # 好友请求 - self.LOG.info(f"收到好友请求:{msg}") - self.auto_accept_friend_request(msg) - - elif msg.type == 10000: # 系统信息 - self.say_hi_to_new_friend(msg) - - elif msg.type == 0x01: # 文本消息 + elif msg.msg_type == MessageType.TEXT: # 文本消息 # 让配置加载更灵活,自己可以更新配置。也可以利用定时任务更新。 if msg.from_self(): if msg.content == "^更新$": @@ -274,79 +221,15 @@ class Robot(Job): else: self.toChitchat(msg) # 闲聊 - def onMsg(self, msg: WxMsg) -> int: + def onMsg(self, msg: WxMessage) -> int: try: - self.LOG.debug(msg) # 打印信息 - self.processMsg(msg) + self.LOG.info(msg) # 打印信息 + # self.processMsg(msg) except Exception as e: self.LOG.error(e) return 0 - def enableRecvMsg(self) -> None: - self.wcf.enable_recv_msg(self.onMsg) - - def enableReceivingMsg(self) -> None: - def innerProcessMsg(wcf: Wcf): - while wcf.is_receiving_msg(): - try: - msg = wcf.get_msg() - self.LOG.debug(msg) - self.processMsg(msg) - except Empty: - continue # Empty message - except Exception as e: - self.LOG.error(f"Receiving message error: {e}") - - self.wcf.enable_receiving_msg() - Thread(target=innerProcessMsg, name="GetMessage", args=(self.wcf,), daemon=True).start() - - def send_text_msg(self, msg: str, receiver: str, at_list: str = "") -> None: - """ 发送消息 - :param msg: 消息字符串 - :param receiver: 接收人wxid或者群id - :param at_list: 要@的wxid, @所有人的wxid为:notify@all - """ - # msg 中需要有 @ 名单中一样数量的 @ - - # 风控处理,随机延迟发送,解决群消息高频发送导致的微信风险 - time.sleep(random.uniform(0.3, 1.0)) - - ats = "" - if at_list: - if at_list == "notify@all": # @所有人 - ats = " @所有人" - else: - wxids = at_list.split(",") - for wxid in wxids: - # 根据 wxid 查找群昵称 - ats += f" @{self.wcf.get_alias_in_chatroom(wxid, receiver)}" - - # {msg}{ats} 表示要发送的消息内容后面紧跟@,例如 北京天气情况为:xxx @张三 - if ats == "": - self.LOG.info(f"To {receiver}: {msg}") - self.wcf.send_text(f"{msg}", receiver, at_list) - else: - self.LOG.info(f"To {receiver}: {ats}\r{msg}") - self.wcf.send_text(f"{ats}\n\n{msg}", receiver, at_list) - - def get_all_contacts(self) -> dict: - """ - 获取联系人(包括好友、公众号、服务号、群成员……) - 格式: {"wxid": "NickName"} - """ - contacts = self.wcf.query_sql("MicroMsg.db", "SELECT UserName, NickName FROM Contact;") - return {contact["UserName"]: contact["NickName"] for contact in contacts} - - def get_all_head_img_url(self) -> dict: - try: - head_img_urls = self.wcf.query_sql("MicroMsg.db", "SELECT usrName ,bigHeadImgUrl FROM ContactHeadImgUrl;") - # self.LOG.info(f"head_img_urls: {head_img_urls}") - return {contact["usrName"]: contact["bigHeadImgUrl"] for contact in head_img_urls} - except Exception as e: - self.LOG.error(f"get_all_head_img_url error: {e}") - return {} - def keep_running_and_block_process(self) -> None: """ 保持机器人运行,不让进程退出 @@ -355,31 +238,11 @@ class Robot(Job): self.runPendingJobs() time.sleep(1) - def auto_accept_friend_request(self, msg: WxMsg) -> None: - try: - xml = ET.fromstring(msg.content) - v3 = xml.attrib["encryptusername"] - v4 = xml.attrib["ticket"] - scene = int(xml.attrib["scene"]) - res = self.wcf.accept_new_friend(v3, v4, scene) - self.LOG.info(f"同意好友请求:{res}") - except Exception as e: - self.LOG.error(f"同意好友出错:{e}") - - def say_hi_to_new_friend(self, msg: WxMsg) -> None: - nickName = re.findall(r"你已添加了(.*),现在可以开始聊天了。", msg.content) - if nickName: - # 添加了好友,更新好友列表和联系人管理器 - self.allContacts[msg.sender] = nickName[0] - self.contact_manager.update_contact(msg.sender, nickName[0]) - self.send_text_msg(f"Hi {nickName[0]},我自动通过了你的好友请求。", msg.sender) - # 添加一个方法用于刷新联系人信息 def refresh_contacts(self): """刷新联系人信息""" self.allContacts = self.get_all_contacts() - self.all_head_img = self.get_all_head_img_url() - self.contact_manager.refresh_contacts(self.allContacts, self.all_head_img, self.wcf) + self.contact_manager.refresh_contacts(self.allContacts) self.LOG.info("联系人信息已刷新") def send_group_txt_message(self, msg: str, feature: Feature): @@ -389,7 +252,7 @@ class Robot(Job): return for r in receivers: if self.gbm.get_group_permission(r, feature) == PermissionStatus.ENABLED: - self.send_text_msg(msg, r) + self.message_util.send_text(msg, r) except Exception as e: self.LOG.error(f"send_group_txt_message:{feature.description} error:{e}") @@ -400,11 +263,11 @@ class Robot(Job): return for r in receivers: if self.gbm.get_group_permission(r, feature) == PermissionStatus.ENABLED: - self.wcf.send_file(path, r) + self.message_util.send_file(path, r) except Exception as e: self.LOG.error(f"send_group_file_message:{feature.description} error:{e}") - def process_plugin_message(self, msg: WxMsg) -> bool: + def process_plugin_message(self, msg: WxMessage) -> bool: """使用插件处理消息""" # 获取所有消息处理插件 message_plugins = self.plugin_registry.get_plugins_by_type(MessagePluginInterface) @@ -415,16 +278,15 @@ class Robot(Job): continue try: - # 转换WxMsg为插件可处理的格式 + # 转换WxMessage为插件可处理的格式 plugin_msg = { - "type": msg.type, + "type": msg.msg_type, "content": msg.content, "sender": msg.sender, "roomid": msg.roomid if msg.from_group() else "", - "xml": msg.xml, + "xml": msg.content.xml_content, "is_at": msg.is_at(self.wxid), # 确保正确设置is_at标志 "timestamp": time.time(), - "wcf": self.wcf, # 提供wcf对象,让插件可以直接发送消息 "message_util": self.message_util, # 提供消息工具类 "gbm": self.gbm, # 每次从程序变量中取,保证最新 "all_contacts": self.allContacts, @@ -536,6 +398,87 @@ class Robot(Job): def xiu_ren_pdf_send(self): try: pub_path = generate_pdf_from_images("xiuren") - self.wcf.send_file(pub_path, "45317011307@chatroom") + self.message_util.send_file(pub_path, "45317011307@chatroom") except Exception as e: self.LOG.error(f"xiu_ren_pdf_send error:{e}") + + def get_all_contacts(self) -> dict: + """获取所有联系人信息并保存到数据库""" + from db.contacts_db import ContactsDBOperator + + contacts_dict = {} + contacts_wxids = self.client.fetch_contacts_list(self.app_id) + + # 初始化联系人数据库操作类 + contacts_db = ContactsDBOperator() + + for wxid in contacts_wxids: + # 获取联系人详细信息 + contact_info = self.client.get_detail_info(self.app_id, wxid) + + # 将联系人信息添加到字典中 + if contact_info and contact_info.get("ret") == 200 and "data" in contact_info: + contact_data = contact_info.get("data", []) + + # 保存联系人信息到数据库 + if contact_data: + try: + # 判断联系人类型 + contact_type = "friends" # 默认为好友类型 + if wxid.endswith("@chatroom"): + contact_type = "chatrooms" + # 如果是这个类型,则提取群成员信息 + # 提取群成员信息 + self.update_chatroom_member_details(wxid) + elif wxid.startswith("gh_"): + contact_type = "ghs" + + # 保存到数据库 + contacts_db.save_contacts(contact_data, contact_type) + + # 添加到返回字典 + for contact in contact_data: + user_name = contact.get("userName") + if user_name: + contacts_dict[user_name] = contact.get("nickName") or user_name + except Exception as e: + self.LOG.error(f"保存联系人信息到数据库失败: {e}") + + self.LOG.info(f"成功获取并保存{len(contacts_dict)}个联系人信息") + return contacts_dict + + def update_chatroom_member_details(self, chatroom_id): + """更新群成员详细信息""" + try: + # 首先获取群成员列表 + members_response = self.client.get_chatroom_member_list(self.app_id, chatroom_id) + if members_response and members_response.get('ret') == 200: + member_list = members_response.get('data', {}).get('memberList', []) + + # 提取成员wxid列表 + member_wxids = [member.get('wxid') for member in member_list if member.get('wxid')] + + if member_wxids: + # 获取成员详细信息 + details_response = self.client.get_chatroom_member_detail(self.app_id, chatroom_id, member_wxids) + + # 使用ContactsDBOperator处理响应 + from db.contacts_db import ContactsDBOperator + contacts_db = ContactsDBOperator() + success = contacts_db.process_chatroom_member_detail_response(chatroom_id, details_response) + + if success: + self.LOG.info(f"成功更新群聊{chatroom_id}的成员详细信息") + else: + self.LOG.error(f"更新群聊{chatroom_id}的成员详细信息失败") + + return success + else: + self.LOG.warning(f"群聊{chatroom_id}没有成员") + return False + else: + self.LOG.error(f"获取群聊{chatroom_id}成员列表失败") + return False + except Exception as e: + self.LOG.error(f"更新群聊成员详细信息出错: {e}") + return False diff --git a/utils/decorator/points_decorator.py b/utils/decorator/points_decorator.py index 3d46d73..8b88709 100644 --- a/utils/decorator/points_decorator.py +++ b/utils/decorator/points_decorator.py @@ -143,7 +143,7 @@ def plugin_points_cost(points: int, description: str = None, feature: Feature = # 积分不足 wcf = message.get("wcf") if wcf: - wcf.send_text( + self.message_util.send_text( f"❌ 积分不足\n无法使用 {plugin_name} 功能\n" f"🪙 先参与积分活动[签到,答题/t]赚取吧!\n" f"💰 有: {user_points['total_points']} | 需: {points} |差: {points - user_points['total_points']} ", @@ -170,7 +170,7 @@ def plugin_points_cost(points: int, description: str = None, feature: Feature = response += f"\n\n💰 已消费 {points} 积分" wcf = message.get("wcf") if wcf: - wcf.send_text( + self.message_util.send_text( f"💰消费 {points} 积分", (roomid if roomid else sender), sender ) diff --git a/utils/json_converter.py b/utils/json_converter.py new file mode 100644 index 0000000..fdbe3fd --- /dev/null +++ b/utils/json_converter.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +""" +JSON数据转换工具 +用于将JSON数据转换为Python对象 +""" + +class DictObject: + """将字典转换为对象,使得可以通过属性访问""" + def __init__(self, data): + for key, value in data.items(): + if isinstance(value, dict): + setattr(self, key, DictObject(value)) + elif isinstance(value, list): + setattr(self, key, [ + DictObject(item) if isinstance(item, dict) else item + for item in value + ]) + else: + setattr(self, key, value) + + def __repr__(self): + """打印对象时的表示形式""" + attrs = ', '.join(f"{key}={repr(value)}" for key, value in self.__dict__.items()) + return f"{self.__class__.__name__}({attrs})" + + def to_dict(self): + """将对象转换回字典""" + result = {} + for key, value in self.__dict__.items(): + if isinstance(value, DictObject): + result[key] = value.to_dict() + elif isinstance(value, list): + result[key] = [ + item.to_dict() if isinstance(item, DictObject) else item + for item in value + ] + else: + result[key] = value + return result + + +def json_to_object(json_data): + """ + 将JSON数据转换为Python对象 + + Args: + json_data: JSON字符串或字典 + + Returns: + DictObject: 转换后的Python对象 + """ + import json + + # 如果输入是字符串,则解析为字典 + if isinstance(json_data, str): + data = json.loads(json_data) + else: + data = json_data + + # 转换为对象 + return DictObject(data) + + +# 使用示例 +if __name__ == "__main__": + # 示例JSON数据 + example_json = { + "ret": 200, + "msg": "操作成功", + "data": { + "friends": [ + "tmessage", + "medianote", + "qmessage", + "qqmail", + "wxid_910acevfm2nb21", + "qqsafe", + "wxid_9299552988412", + "weixin", + "exmail_tool", + "wxid_mp05xmje0ctn22", + "wxid_09oq4f4j4wg912", + "wxid_6bfguz79h8n122", + "wxid_lyuq4hr4lrjq22", + "wxid_a1zqyljsrsdu12", + "wxid_lv3pb3zhna3522", + "wxid_k2biq6fuinsr22", + "wxid_ujredjhxz9y712", + "wxid_uwb7989u0jea12", + "wxid_in46ey732vxu12", + "wxid_3rvervwohj6921", + "wxid_4wkls7tu62ua12", + "wxid_g0bdknnotx2f12", + "wxid_ce5fgp0icb3y21", + "wxid_1482424825211", + "wxid_vw3p4f6jy7bm12", + "wxid_o2m8xm71c23522", + "wxid_bclqpc2ho6o412", + "wxid_98pjjzpiisi721", + "wxid_noq2wsn5c8h222" + ], + "chatrooms": [ + "2180313478@chatroom", + "14358945067@chatroom", + "17362526147@chatroom", + "11685224357@chatroom", + "17522822550@chatroom" + ], + "ghs": [ + "gh_7aac992b0363", + "gh_d7293b5f14f4", + "gh_f51ce3ef83a4", + "gh_7d20df86e26b", + "gh_69bfb92a3e43" + ] + } + } + + # 转换为对象 + obj = json_to_object(example_json) + + # 通过属性访问 + print(f"返回码: {obj.ret}") + print(f"消息: {obj.msg}") + print(f"好友数量: {len(obj.data.friends)}") + print(f"第一个好友: {obj.data.friends[0]}") + print(f"第一个群聊: {obj.data.chatrooms[0]}") + + # 转换回字典 + dict_data = obj.to_dict() + print(f"转换回字典: {dict_data}") \ No newline at end of file diff --git a/utils/robot_cmd/robot_command.py b/utils/robot_cmd/robot_command.py index 9538257..6461ed9 100644 --- a/utils/robot_cmd/robot_command.py +++ b/utils/robot_cmd/robot_command.py @@ -41,7 +41,7 @@ class Feature(Enum): VIDEO = 14, "黑丝视频 [黑丝视频, 黑丝, 来个黑丝,搞个黑丝]" VIDEO_MAN = 15, "肌肉视频 [猛男, 肌肉, 帅哥]" # GROUP_ADD = 16, "加群提醒" - # DOUYIN_PARSER = 17, "抖音链接转视频" + DOUYIN_PARSER = 17, "抖音链接转视频" GROUP_MEMBER_CHANGE = 18, "群成员变更提醒功能" # KID_PHOTO_EXTRACT = 19, "儿童照片提取转发功能" # 小朋友照片提取功能 NEWS = 20, "全球政治经济新闻" diff --git a/utils/wechat/contact_manager.py b/utils/wechat/contact_manager.py index dbb0775..31f081a 100644 --- a/utils/wechat/contact_manager.py +++ b/utils/wechat/contact_manager.py @@ -5,7 +5,9 @@ import logging from typing import Dict, Optional, List, Tuple -from wcferry import Wcf +from gewechat_client import GewechatClient + +from utils.json_converter import json_to_object class ContactManager: @@ -50,7 +52,7 @@ class ContactManager: cls._instance = ContactManager() return cls._instance - def set_contacts(self, contacts: Dict[str, str], head_imgs: Dict[str, str], wcf: Wcf) -> None: + def set_contacts(self, contacts: Dict[str, str]) -> None: """设置联系人字典 Args: @@ -67,13 +69,12 @@ class ContactManager: "gender": gender} """ self._contacts = contacts - self._friends = wcf.get_friends() - self._head_images = head_imgs + self._friends = contacts self._logger.info(f"联系人信息已更新,共 {len(contacts)} 个联系人") # 分类联系人 - self._classify_contacts(wcf) + self._classify_contacts() - def _classify_contacts(self, wcf: Wcf) -> None: + def _classify_contacts(self) -> None: """将联系人分类为群组、个人联系人、公共好友和公众号""" self._group_contacts = {} self._personal_contacts = {} @@ -90,8 +91,6 @@ class ContactManager: # 判断是否为群组(wxid以@chatroom结尾) elif wxid.endswith('@chatroom'): self._group_contacts[wxid] = nickname - # 如果是群,这处理群列表内容 - self._group_contacts_friends[wxid] = wcf.get_chatroom_members(wxid) # # 其他为普通好友和群成员 # else: @@ -216,7 +215,7 @@ class ContactManager: self._personal_contacts[wxid] = nickname self._logger.debug(f"已更新联系人: {wxid} -> {nickname}") - def refresh_contacts(self, new_contacts: Dict[str, str], head_imgs: Dict[str, str], wcf: Wcf) -> None: + def refresh_contacts(self, new_contacts: Dict[str, str]) -> None: """刷新联系人信息 Args: @@ -235,11 +234,9 @@ class ContactManager: # "province": cnt.get("province", ""), # "city": cnt.get("city", ""), # "gender": gender} - self._friends = wcf.get_friends() - self._head_images = head_imgs + self._friends = self.message_util.get_friends() self._logger.info(f"联系人信息已刷新,共 {len(new_contacts)} 个联系人") - # 重新分类联系人 - self._classify_contacts(wcf) + self._classify_contacts() def get_contact_statistics(self) -> Tuple[int, int, int, int, int]: """获取联系人统计信息 diff --git a/utils/wechat/message_to_db.py b/utils/wechat/message_to_db.py index 47d2566..66b6d36 100644 --- a/utils/wechat/message_to_db.py +++ b/utils/wechat/message_to_db.py @@ -3,12 +3,15 @@ import xml.etree.ElementTree as ET import logging import concurrent.futures # 添加线程池支持 import os -from wcferry import WxMsg, Wcf + +from gewechat_client import GewechatClient from db.connection import DBConnectionManager from db.message_storage import MessageStorageDB # 导入积分系统 from db.points_db import PointsDBOperator, PointSource +from gewechat.call_back_message.message import WxMessage + # 配置日志 logging.basicConfig( level=logging.INFO, @@ -19,11 +22,11 @@ logger = logging.getLogger("MessageStorage") class MessageStorage: - def __init__(self, wcf: Wcf = None): + def __init__(self, client: GewechatClient = None): # 获取数据库连接管理器的单例 self.db_manager = DBConnectionManager.get_instance() self.message_db = MessageStorageDB(self.db_manager) - + self.points_db = PointsDBOperator(self.db_manager) # 初始化本地缓存字典,使用 group_id 作为键 self.local_membercounts = {} @@ -34,7 +37,7 @@ class MessageStorage: self.pending_tasks = [] # 保存WCF实例,用于图片处理 - self.wcf = wcf + self.client = client # 图片处理相关初始化 self.image_executor = concurrent.futures.ThreadPoolExecutor(max_workers=3) # 专用于图片处理的线程池 @@ -46,7 +49,7 @@ class MessageStorage: os.makedirs(self.image_dir, exist_ok=True) logger.info(f"图片存储目录: {self.image_dir}") - def process_message(self, message: WxMsg): + def process_message(self, message: WxMessage): # 示例message字符串 current_date = datetime.now().strftime('%Y-%m-%d') # 生成Redis key @@ -59,7 +62,7 @@ class MessageStorage: redis_conn.expire(key, 86400 * 2) # 或者使用字符串:r.incr(key) # 如果只存储一个整数值,字符串类型可能更简单 - def archive_message(self, msg: WxMsg): + def archive_message(self, msg: WxMessage): """异步存档消息,防止堵塞主线程""" # 提交任务到线程池 future = self.executor.submit(self._archive_message_task, msg) @@ -70,7 +73,7 @@ class MessageStorage: # 清理已完成的任务 self._cleanup_completed_tasks() - def _archive_message_task(self, msg: WxMsg): + def _archive_message_task(self, msg: WxMessage): """实际执行消息存档的任务函数""" try: # 使用 MessageStorageDB 类存档消息 @@ -80,7 +83,7 @@ class MessageStorage: 'roomid': msg.roomid, 'sender': msg.sender, 'content': msg.content, # 添加消息内容 - 'message_id': msg.id # 添加消息ID + 'message_id': msg.msg_id # 添加消息ID } except Exception as e: logger.error(f"存档消息出错: {e}") @@ -89,13 +92,13 @@ class MessageStorage: 'roomid': msg.roomid, 'sender': msg.sender, 'content': msg.content, # 添加消息内容 - 'message_id': msg.id, # 添加消息ID + 'message_id': msg.msg_id, # 添加消息ID 'error': str(e) } - def process_image(self, msg: WxMsg): + def process_image(self, msg: WxMessage): """异步处理图片消息,与消息存档分离""" - if msg.type != 3 or not self.wcf: # 不是图片消息或没有WCF实例 + if msg.msg_type != 3 or not self.client: # 不是图片消息或没有WCF实例 return False # 提交任务到图片处理线程池 @@ -108,11 +111,11 @@ class MessageStorage: self._cleanup_completed_tasks() return True - def _process_image_task(self, msg: WxMsg): + def _process_image_task(self, msg: WxMessage): """实际执行图片处理的任务函数""" try: # 使用wcf下载图片,确保图片存在 - if self.wcf and msg.id: + if self.client and msg.msg_id: # 创建按群ID或个人wxid分割的目录 target_dir = os.path.join(self.image_dir, msg.roomid if msg.roomid else msg.sender) # 确保目标目录存在 @@ -120,16 +123,27 @@ class MessageStorage: os.makedirs(target_dir, exist_ok=True) # 尝试使用wcf下载图片到分组后的目录 - download_path = self.wcf.download_image(msg.id, msg.extra, target_dir) + json = self.client.download_image(msg.msg_id, msg.content.xml_content, 1) + # { + # "ret": 200, + # "msg": "操作成功", + # "data": { + # "fileUrl": "/download/20240720/wx_BTVoJ_o_r6DpxNCNiycFE/0ca5b675-8e2c-4dc1-b288-3c44a40086ec4" + # } + # } + # 解析JSON + if json and json.get('data') and json['data'].get('fileUrl'): + file_url = json['data']['fileUrl'] + download_path = self.download_file_from_url(file_url, target_dir) if download_path: - logger.info(f"使用wcf下载图片成功: {msg.id} -> {download_path}") + logger.info(f"使用wcf下载图片成功: {msg.msg_id} -> {download_path}") # 直接使用下载后的路径更新数据库 - self.message_db.update_message_image_path(msg.id, download_path) + self.message_db.update_message_image_path(msg.msg_id, download_path) return { 'success': True, - 'message_id': msg.id, + 'message_id': msg.msg_id, 'roomid': msg.roomid, 'sender': msg.sender, 'file_path': download_path @@ -137,7 +151,7 @@ class MessageStorage: else: return { 'success': False, - 'message_id': msg.id, + 'message_id': msg.msg_id, 'roomid': msg.roomid, 'sender': msg.sender, 'error': "图片下载失败" @@ -145,21 +159,25 @@ class MessageStorage: else: return { 'success': False, - 'message_id': msg.id, + 'message_id': msg.msg_id, 'roomid': msg.roomid, 'sender': msg.sender, 'error': "WCF实例不存在或消息ID无效" } except Exception as e: - logger.error(f"图片处理出错: {msg.id}, 错误: {e}") + logger.error(f"图片处理出错: {msg.msg_id}, 错误: {e}") return { 'success': False, - 'message_id': msg.id, + 'message_id': msg.msg_id, 'roomid': msg.roomid, 'sender': msg.sender, 'error': str(e) } + def download_file_from_url(self, url: str, target_dir: str) -> str: + # TODO 根据获取的文件地址,从server 下载 :http://{服务ip}:2532/download/{接口返回的文件路径} + return "" + def _process_image_callback(self, future): """处理异步图片处理任务完成后的回调""" try: @@ -256,13 +274,13 @@ class MessageStorage: # 格式化输出字符串,添加emoji和美化格式 ranking_str = f"🏆 {yesterday} 发言排行榜 🏆\n" - + # 为不同名次添加不同的奖杯和样式,并发放积分 for rank, result in enumerate(results, start=1): username = result['wx_id'] speech_count = result['speech_count'] display_name = allContacts.get(username, username) - + # 根据排名发放不同数量的积分 reward_points = 0 if rank == 1: @@ -280,14 +298,14 @@ class MessageStorage: else: reward_points = 5 ranking_str += f"👍 {rank}.{display_name}: {speech_count}次 +{reward_points}积分\n" - + # 发放积分奖励 if reward_points > 0: success, _ = self.points_db.add_points( - username, - groupId, - reward_points, - PointSource.OTHER, + username, + groupId, + reward_points, + PointSource.OTHER, f"{yesterday}发言排行第{rank}名奖励" ) if not success: