diff --git a/commands/handlers.py b/commands/handlers.py index 39a54d7..ae8c918 100644 --- a/commands/handlers.py +++ b/commands/handlers.py @@ -580,6 +580,82 @@ def handle_chitchat(ctx: 'MessageContext', match: Optional[Match]) -> bool: ctx.send_text("抱歉,我现在无法进行对话。") return False + # ---- 处理引用图片情况 ---- + if getattr(ctx, 'is_quoted_image', False): + ctx.logger.info("检测到引用图片消息,尝试处理图片内容...") + + import os + from ai_providers.ai_chatgpt import ChatGPT + + # 确保是 ChatGPT 类型且支持图片处理 + support_vision = False + if isinstance(chat_model, ChatGPT): + if hasattr(chat_model, 'support_vision') and chat_model.support_vision: + support_vision = True + else: + # 检查模型名称判断是否支持视觉 + if hasattr(chat_model, 'model'): + model_name = getattr(chat_model, 'model', '') + support_vision = model_name == "gpt-4.1-mini" or model_name == "gpt-4o" or "-vision" in model_name + + if not support_vision: + ctx.send_text("抱歉,当前 AI 模型不支持处理图片。请联系管理员配置支持视觉的模型 (如 gpt-4-vision-preview、gpt-4o 等)。") + return True + + # 下载图片并处理 + try: + # 创建临时目录 + temp_dir = "temp/image_cache" + os.makedirs(temp_dir, exist_ok=True) + + # 下载图片 + ctx.logger.info(f"正在下载引用图片: msg_id={ctx.quoted_msg_id}") + image_path = ctx.wcf.download_image( + id=ctx.quoted_msg_id, + extra=ctx.quoted_image_extra, + dir=temp_dir, + timeout=30 + ) + + if not image_path or not os.path.exists(image_path): + ctx.logger.error(f"图片下载失败: {image_path}") + ctx.send_text("抱歉,无法下载图片进行分析。") + return True + + ctx.logger.info(f"图片下载成功: {image_path},准备分析...") + + # 调用 ChatGPT 分析图片 + try: + # 根据用户的提问构建 prompt + prompt = ctx.text + if not prompt or prompt.strip() == "": + prompt = "请详细描述这张图片中的内容" + + # 调用图片分析函数 + response = chat_model.get_image_description(image_path, prompt) + ctx.send_text(response) + + ctx.logger.info("图片分析完成并已发送回复") + except Exception as e: + ctx.logger.error(f"分析图片时出错: {e}") + ctx.send_text(f"分析图片时出错: {str(e)}") + + # 清理临时图片 + try: + if os.path.exists(image_path): + os.remove(image_path) + ctx.logger.info(f"临时图片已删除: {image_path}") + except Exception as e: + ctx.logger.error(f"删除临时图片出错: {e}") + + return True # 已处理,不执行后续的普通文本处理流程 + + except Exception as e: + ctx.logger.error(f"处理引用图片过程中出错: {e}") + ctx.send_text(f"处理图片时发生错误: {str(e)}") + return True # 已处理,即使出错也不执行后续普通文本处理 + # ---- 引用图片处理结束 ---- + # 获取消息内容 content = ctx.text sender_name = ctx.sender_name @@ -587,7 +663,6 @@ def handle_chitchat(ctx: 'MessageContext', match: Optional[Match]) -> bool: # 使用XML处理器格式化消息 if ctx.robot and hasattr(ctx.robot, "xml_processor"): # 创建格式化的聊天内容(带有引用消息等) - # 原始代码中是从xml_processor获取的 if ctx.is_group: # 处理群聊消息 msg_data = ctx.robot.xml_processor.extract_quoted_message(ctx.msg) diff --git a/function/func_xml_process.py b/function/func_xml_process.py index a973b0d..60a764e 100644 --- a/function/func_xml_process.py +++ b/function/func_xml_process.py @@ -127,11 +127,47 @@ class XmlProcessor: # 提取refermsg内容 refer_data = self.extract_refermsg(msg.content) result["quoted_sender"] = refer_data.get("sender", "") - result["quoted_content"] = refer_data.get("content", "") + + # 新增代码开始 + is_quoted_image = False + quoted_msg_id = None + quoted_image_extra = None + + # 尝试从原始消息内容中解析 refermsg 结构,获取引用类型和svrid + refermsg_match = re.search(r'(.*?)', msg.content, re.DOTALL) + if refermsg_match: + refermsg_inner_xml = refermsg_match.group(1) + refer_type_match = re.search(r'(\d+)', refermsg_inner_xml) + refer_svrid_match = re.search(r'(\d+)', refermsg_inner_xml) + + if refer_type_match and refer_type_match.group(1) == '3' and refer_svrid_match: + # 确认是引用图片 (type=3) + is_quoted_image = True + try: + quoted_msg_id = int(refer_svrid_match.group(1)) + # refer_data["raw_content"] 应该就是解码后的 XML + quoted_image_extra = refer_data.get("raw_content", "") + self.logger.info(f"识别到引用图片消息,原消息ID: {quoted_msg_id}") + except ValueError: + self.logger.error(f"无法将svrid '{refer_svrid_match.group(1)}' 转换为整数") + except Exception as e: + self.logger.error(f"提取引用图片信息时出错: {e}") + + if is_quoted_image and quoted_msg_id is not None and quoted_image_extra: + # 如果是引用图片,更新 result 字典 + result["media_type"] = "引用图片" # 更新媒体类型 + result["quoted_msg_id"] = quoted_msg_id # 存储原图片消息 ID + result["quoted_image_extra"] = quoted_image_extra # 存储原图片消息 XML (用于下载) + result["quoted_content"] = "[引用的图片]" # 使用占位符文本 + result["quoted_is_card"] = False # 明确不是卡片 + else: + # 原有的代码继续 + result["quoted_content"] = refer_data.get("content", "") + # 新增代码结束 # 从raw_content尝试解析被引用内容的卡片信息 raw_content = refer_data.get("raw_content", "") - if raw_content and "(.*?)', msg.content, re.DOTALL) + if refermsg_match: + refermsg_inner_xml = refermsg_match.group(1) + refer_type_match = re.search(r'(\d+)', refermsg_inner_xml) + refer_svrid_match = re.search(r'(\d+)', refermsg_inner_xml) + + if refer_type_match and refer_type_match.group(1) == '3' and refer_svrid_match: + # 确认是引用图片 (type=3) + is_quoted_image = True + try: + quoted_msg_id = int(refer_svrid_match.group(1)) + # refer_data["raw_content"] 应该就是解码后的 XML + quoted_image_extra = refer_data.get("raw_content", "") + self.logger.info(f"识别到引用图片消息,原消息ID: {quoted_msg_id}") + except ValueError: + self.logger.error(f"无法将svrid '{refer_svrid_match.group(1)}' 转换为整数") + except Exception as e: + self.logger.error(f"提取引用图片信息时出错: {e}") + + if is_quoted_image and quoted_msg_id is not None and quoted_image_extra: + # 如果是引用图片,更新 result 字典 + result["media_type"] = "引用图片" # 更新媒体类型 + result["quoted_msg_id"] = quoted_msg_id # 存储原图片消息 ID + result["quoted_image_extra"] = quoted_image_extra # 存储原图片消息 XML (用于下载) + result["quoted_content"] = "[引用的图片]" # 使用占位符文本 + result["quoted_is_card"] = False # 明确不是卡片 + else: + # 原有的代码继续 + result["quoted_content"] = refer_data.get("content", "") + # 新增代码结束 # 从raw_content尝试解析被引用内容的卡片信息 raw_content = refer_data.get("raw_content", "") - if raw_content and "" in msg.content or " int + + 通过好友申请 + + :param v3: 加密用户名 (好友申请消息里 v3 开头的字符串) + :type v3: str + :param v4: Ticket (好友申请消息里 v4 开头的字符串) + :type v4: str + :param scene: 申请方式 (好友申请消息里的 scene); 为了兼容旧接口,默认为扫码添加 (30) + + :returns: 1 为成功,其他失败 + :rtype: int + + + + .. py:method:: add_chatroom_members(roomid: str, wxids: str) -> int + + 添加群成员 + + :param roomid: 待加群的 id + :type roomid: str + :param wxids: 要加到群里的 wxid,多个用逗号分隔 + :type wxids: str + + :returns: 1 为成功,其他失败 + :rtype: int + + + + .. py:method:: cleanup() -> None + + 关闭连接,回收资源 + + + + .. py:method:: decrypt_image(src: str, dir: str) -> str + + 解密图片。这方法别直接调用,下载图片使用 `download_image`。 + + :param src: 加密的图片路径 + :type src: str + :param dir: 保存图片的目录 + :type dir: str + + :returns: 解密图片的保存路径 + :rtype: str + + + + .. py:method:: del_chatroom_members(roomid: str, wxids: str) -> int + + 删除群成员 + + :param roomid: 群的 id + :type roomid: str + :param wxids: 要删除成员的 wxid,多个用逗号分隔 + :type wxids: str + + :returns: 1 为成功,其他失败 + :rtype: int + + + + .. py:method:: disable_recv_msg() -> int + + 停止接收消息 + + + + .. py:method:: download_attach(id: int, thumb: str, extra: str) -> int + + 下载附件(图片、视频、文件)。这方法别直接调用,下载图片使用 `download_image`。 + + :param id: 消息中 id + :type id: int + :param thumb: 消息中的 thumb + :type thumb: str + :param extra: 消息中的 extra + :type extra: str + + :returns: 0 为成功, 其他失败。 + :rtype: int + + + + .. py:method:: download_image(id: int, extra: str, dir: str, timeout: int = 30) -> str + + 下载图片 + + :param id: 消息中 id + :type id: int + :param extra: 消息中的 extra + :type extra: str + :param dir: 存放图片的目录(目录不存在会出错) + :type dir: str + :param timeout: 超时时间(秒) + :type timeout: int + + :returns: 成功返回存储路径;空字符串为失败,原因见日志。 + :rtype: str + + + + .. py:method:: download_video(id: int, thumb: str, dir: str, timeout: int = 30) -> str + + 下载视频 + + :param id: 消息中 id + :type id: int + :param thumb: 消息中的 thumb(即视频的封面图) + :type thumb: str + :param dir: 存放视频的目录(目录不存在会出错) + :type dir: str + :param timeout: 超时时间(秒) + :type timeout: int + + :returns: 成功返回存储路径;空字符串为失败,原因见日志。 + :rtype: str + + + + .. py:method:: enable_receiving_msg(pyq=False) -> bool + + 允许接收消息,成功后通过 `get_msg` 读取消息 + + + + .. py:method:: enable_recv_msg(callback: Callable[[wcferry.wxmsg.WxMsg], None] = None) -> bool + + (不建议使用)设置接收消息回调,消息量大时可能会丢失消息 + + .. deprecated:: 3.7.0.30.13 + + + + .. py:method:: forward_msg(id: int, receiver: str) -> int + + 转发消息。可以转发文本、图片、表情、甚至各种 XML; + 语音也行,不过效果嘛,自己验证吧。 + + :param id: 待转发消息的 id + :type id: str + :param receiver: 消息接收者,wxid 或者 roomid + :type receiver: str + + :returns: 1 为成功,其他失败 + :rtype: int + + + + .. py:method:: get_alias_in_chatroom(wxid: str, roomid: str) -> str + + 获取群名片 + + :param wxid: wxid + :type wxid: str + :param roomid: 群的 id + :type roomid: str + + :returns: 群名片 + :rtype: str + + + + .. py:method:: get_audio_msg(id: int, dir: str, timeout: int = 3) -> str + + 获取语音消息并转成 MP3 + :param id: 语音消息 id + :type id: int + :param dir: MP3 保存目录(目录不存在会出错) + :type dir: str + :param timeout: 超时时间(秒) + :type timeout: int + + :returns: 成功返回存储路径;空字符串为失败,原因见日志。 + :rtype: str + + + + .. py:method:: get_chatroom_members(roomid: str) -> Dict + + 获取群成员 + + :param roomid: 群的 id + :type roomid: str + + :returns: 群成员列表: {wxid1: 昵称1, wxid2: 昵称2, ...} + :rtype: Dict + + + + .. py:method:: get_contacts() -> List[Dict] + + 获取完整通讯录 + + + + .. py:method:: get_dbs() -> List[str] + + 获取所有数据库 + + + + .. py:method:: get_friends() -> List[Dict] + + 获取好友列表 + + + + .. py:method:: get_info_by_wxid(wxid: str) -> dict + + 通过 wxid 查询微信号昵称等信息 + + :param wxid: 联系人 wxid + :type wxid: str + + :returns: {wxid, code, name, gender} + :rtype: dict + + + + .. py:method:: get_msg(block=True) -> wcferry.wxmsg.WxMsg + + 从消息队列中获取消息 + + :param block: 是否阻塞,默认阻塞 + :type block: bool + + :returns: 微信消息 + :rtype: WxMsg + + :raises Empty: 如果阻塞并且超时,抛出空异常,需要用户自行捕获 + + + + .. py:method:: get_msg_types() -> Dict + + 获取所有消息类型 + + + + .. py:method:: get_ocr_result(extra: str, timeout: int = 2) -> str + + 获取 OCR 结果。鸡肋,需要图片能自动下载;通过下载接口下载的图片无法识别。 + + :param extra: 待识别的图片路径,消息里的 extra + :type extra: str + + :returns: OCR 结果 + :rtype: str + + + + .. py:method:: get_qrcode() -> str + + 获取登录二维码,已经登录则返回空字符串 + + + + .. py:method:: get_self_wxid() -> str + + 获取登录账户的 wxid + + + + .. py:method:: get_tables(db: str) -> List[Dict] + + 获取 db 中所有表 + + :param db: 数据库名(可通过 `get_dbs` 查询) + :type db: str + + :returns: `db` 下的所有表名及对应建表语句 + :rtype: List[Dict] + + + + .. py:method:: get_user_info() -> Dict + + 获取登录账号个人信息 + + + + .. py:method:: invite_chatroom_members(roomid: str, wxids: str) -> int + + 邀请群成员 + + :param roomid: 群的 id + :type roomid: str + :param wxids: 要邀请成员的 wxid, 多个用逗号`,`分隔 + :type wxids: str + + :returns: 1 为成功,其他失败 + :rtype: int + + + + .. py:method:: is_login() -> bool + + 是否已经登录 + + + + .. py:method:: is_receiving_msg() -> bool + + 是否已启动接收消息功能 + + + + .. py:method:: keep_running() + + 阻塞进程,让 RPC 一直维持连接 + + + + .. py:method:: query_sql(db: str, sql: str) -> List[Dict] + + 执行 SQL,如果数据量大注意分页,以免 OOM + + :param db: 要查询的数据库 + :type db: str + :param sql: 要执行的 SQL + :type sql: str + + :returns: 查询结果 + :rtype: List[Dict] + + + + .. py:method:: receive_transfer(wxid: str, transferid: str, transactionid: str) -> int + + 接收转账 + + :param wxid: 转账消息里的发送人 wxid + :type wxid: str + :param transferid: 转账消息里的 transferid + :type transferid: str + :param transactionid: 转账消息里的 transactionid + :type transactionid: str + + :returns: 1 为成功,其他失败 + :rtype: int + + + + .. py:method:: refresh_pyq(id: int = 0) -> int + + 刷新朋友圈 + + :param id: 开始 id,0 为最新页 + :type id: int + + :returns: 1 为成功,其他失败 + :rtype: int + + + + .. py:method:: revoke_msg(id: int = 0) -> int + + 撤回消息 + + :param id: 待撤回消息的 id + :type id: int + + :returns: 1 为成功,其他失败 + :rtype: int + + + + .. py:method:: send_emotion(path: str, receiver: str) -> int + + 发送表情 + + :param path: 本地表情路径,如:`C:/Projs/WeChatRobot/emo.gif` + :type path: str + :param receiver: 消息接收人,wxid 或者 roomid + :type receiver: str + + :returns: 0 为成功,其他失败 + :rtype: int + + + + .. py:method:: send_file(path: str, receiver: str) -> int + + 发送文件,非线程安全 + + :param path: 本地文件路径,如:`C:/Projs/WeChatRobot/README.MD` 或 `https://raw.githubusercontent.com/lich0821/WeChatFerry/master/README.MD` + :type path: str + :param receiver: 消息接收人,wxid 或者 roomid + :type receiver: str + + :returns: 0 为成功,其他失败 + :rtype: int + + + + .. py:method:: send_image(path: str, receiver: str) -> int + + 发送图片,非线程安全 + + :param path: 图片路径,如:`C:/Projs/WeChatRobot/TEQuant.jpeg` 或 `https://raw.githubusercontent.com/lich0821/WeChatFerry/master/assets/TEQuant.jpg` + :type path: str + :param receiver: 消息接收人,wxid 或者 roomid + :type receiver: str + + :returns: 0 为成功,其他失败 + :rtype: int + + + + .. py:method:: send_pat_msg(roomid: str, wxid: str) -> int + + 拍一拍群友 + + :param roomid: 群 id + :type roomid: str + :param wxid: 要拍的群友的 wxid + :type wxid: str + + :returns: 1 为成功,其他失败 + :rtype: int + + + + .. py:method:: send_rich_text(name: str, account: str, title: str, digest: str, url: str, thumburl: str, receiver: str) -> int + + 发送富文本消息 + 卡片样式: + |-------------------------------------| + |title, 最长两行 + |(长标题, 标题短的话这行没有) + |digest, 最多三行,会占位 |--------| + |digest, 最多三行,会占位 |thumburl| + |digest, 最多三行,会占位 |--------| + |(account logo) name + |-------------------------------------| + :param name: 左下显示的名字 + :type name: str + :param account: 填公众号 id 可以显示对应的头像(gh_ 开头的) + :type account: str + :param title: 标题,最多两行 + :type title: str + :param digest: 摘要,三行 + :type digest: str + :param url: 点击后跳转的链接 + :type url: str + :param thumburl: 缩略图的链接 + :type thumburl: str + :param receiver: 接收人, wxid 或者 roomid + :type receiver: str + + :returns: 0 为成功,其他失败 + :rtype: int + + + + .. py:method:: send_text(msg: str, receiver: str, aters: Optional[str] = '') -> int + + 发送文本消息 + + :param msg: 要发送的消息,换行使用 `\\n` (单杠);如果 @ 人的话,需要带上跟 `aters` 里数量相同的 @ + :type msg: str + :param receiver: 消息接收人,wxid 或者 roomid + :type receiver: str + :param aters: 要 @ 的 wxid,多个用逗号分隔;`@所有人` 只需要 `notify@all` + :type aters: str + + :returns: 0 为成功,其他失败 + :rtype: int + + + + .. py:method:: send_xml(receiver: str, xml: str, type: int, path: str = None) -> int + + 发送 XML + + :param receiver: 消息接收人,wxid 或者 roomid + :type receiver: str + :param xml: xml 内容 + :type xml: str + :param type: xml 类型,如:0x21 为小程序 + :type type: int + :param path: 封面图片路径 + :type path: str + + :returns: 0 为成功,其他失败 + :rtype: int + + + + .. py:attribute:: LOG + + + .. py:attribute:: cmd_socket + + + .. py:attribute:: cmd_url + :value: 'tcp://None:10086' + + + + .. py:attribute:: contacts + :value: [] + + + + .. py:attribute:: host + :value: None + + + + .. py:attribute:: msgQ + + + .. py:attribute:: msg_socket + + + .. py:attribute:: msg_url + :value: '' + + + + .. py:attribute:: port + :value: 10086 + + + + .. py:attribute:: sdk + :value: None + + + + .. py:attribute:: self_wxid + :value: '' + +