diff --git a/EvoScientist/EvoScientist.py b/EvoScientist/EvoScientist.py index 6b43e14..28d844e 100644 --- a/EvoScientist/EvoScientist.py +++ b/EvoScientist/EvoScientist.py @@ -179,7 +179,6 @@ prompt_refs = { base_middleware = [ create_memory_middleware(MEMORY_DIR, extraction_model=chat_model), - create_skills_middleware(backend), ] # Default agent (no checkpointer) — used by langgraph dev / LangSmith / notebooks. @@ -247,7 +246,6 @@ def create_cli_agent(workspace_dir: str | None = None, checkpointer=None): mw = [ create_memory_middleware(MEMORY_DIR, extraction_model=chat_model), - create_skills_middleware(be), ] # Re-load MCP tools from current config (picks up /mcp add changes) diff --git a/EvoScientist/channels/consumer.py b/EvoScientist/channels/consumer.py index 19e379c..a3ac15c 100644 --- a/EvoScientist/channels/consumer.py +++ b/EvoScientist/channels/consumer.py @@ -275,7 +275,7 @@ class InboundConsumer: await channel.start_typing(msg.chat_id) async for event in _timeout_aiter( - stream_agent_events(self.agent, msg.content, thread_id), + stream_agent_events(self.agent, msg.content, thread_id, media=msg.media or None), self._inference_timeout, ): event_type = event.get("type") diff --git a/EvoScientist/channels/dingtalk/channel.py b/EvoScientist/channels/dingtalk/channel.py index f407078..cbbb750 100644 --- a/EvoScientist/channels/dingtalk/channel.py +++ b/EvoScientist/channels/dingtalk/channel.py @@ -21,6 +21,7 @@ TOKEN_URL = "https://api.dingtalk.com/v1.0/oauth2/accessToken" SEND_URL = "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend" MEDIA_SEND_URL = "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend" MEDIA_UPLOAD_URL = "https://oapi.dingtalk.com/media/upload" +FILE_DOWNLOAD_URL = "https://api.dingtalk.com/v1.0/robot/messageFiles/download" @dataclass @@ -67,6 +68,23 @@ class DingTalkChannel(Channel, WebSocketMixin, TokenMixin): resp = await self._http_client.post(url, json=body, headers=headers) return resp.json() + async def _resolve_download_code(self, download_code: str) -> str | None: + """Exchange a DingTalk downloadCode for a real download URL.""" + try: + token = await self._ensure_token() + data = await self._api_post( + FILE_DOWNLOAD_URL, + {"downloadCode": download_code, "robotCode": self.config.client_id}, + headers={"x-acs-dingtalk-access-token": token}, + ) + url = data.get("downloadUrl") or "" + if url: + return url + logger.warning(f"DingTalk downloadCode resolve failed: {data}") + except Exception as e: + logger.warning(f"DingTalk downloadCode resolve error: {e}") + return None + # ── WebSocketMixin ──────────────────────────────────────────── async def _get_ws_url(self) -> str: @@ -103,19 +121,67 @@ class DingTalkChannel(Channel, WebSocketMixin, TokenMixin): payload = json.loads(payload) if isinstance(payload, str) else payload text_obj = payload.get("text", {}) content = (text_obj.get("content", "") if isinstance(text_obj, dict) else str(text_obj)).strip() - content = content or payload.get("content", "").strip() if not content: - return + raw_content = payload.get("content", "") + content = raw_content.strip() if isinstance(raw_content, str) else "" # Download attachments if present annotations: list[str] = [] media_paths: list[str] = [] + + # DingTalk file/image messages may put download info in + # payload["content"] (as a dict) instead of in a dedicated + # "fileContent"/"imageContent" key. + raw_content_obj = payload.get("content") + if isinstance(raw_content_obj, dict) and raw_content_obj not in [ + payload.get(k) for k in ("imageContent", "fileContent", "videoContent", "audioContent") + ]: + msg_type = payload.get("msgtype") or payload.get("msgType") or "" + media_label = msg_type or "file" + file_size = raw_content_obj.get("fileSize") or raw_content_obj.get("downloadSize") or 0 + file_name = raw_content_obj.get("fileName") or raw_content_obj.get("name") or f"dingtalk_{msg_type}" + download_code = raw_content_obj.get("downloadCode") or "" + download_url = raw_content_obj.get("downloadUrl") or "" + # downloadCode is NOT a URL — resolve it via DingTalk API first + if download_code and not download_code.startswith("http"): + resolved = await self._resolve_download_code(download_code) + if resolved: + download_url = resolved + elif download_code: + download_url = download_code + if download_url: + try: + dl_token = await self._ensure_token() + dl_headers = {"x-acs-dingtalk-access-token": dl_token} + except Exception: + dl_headers = None + local, ann = await self._download_attachment( + download_url, f"dingtalk_{file_name}", + headers=dl_headers, + file_size=int(file_size) if file_size else None, + ) + if local: + media_paths.append(local) + if ann: + ann = ann.replace("[attachment:", f"[{media_label}:") + annotations.append(ann) + elif file_name: + annotations.append(f"[{media_label}: {file_name}]") + for att_key in ("imageContent", "fileContent", "videoContent", "audioContent"): att = payload.get(att_key) if att and isinstance(att, dict): file_size = att.get("fileSize") or att.get("downloadSize") or 0 file_name = att.get("fileName", att_key) - download_url = att.get("downloadCode") or att.get("downloadUrl") or "" + download_code = att.get("downloadCode") or "" + download_url = att.get("downloadUrl") or "" + # Resolve downloadCode via API if it's not a URL + if download_code and not download_code.startswith("http"): + resolved = await self._resolve_download_code(download_code) + if resolved: + download_url = resolved + elif download_code: + download_url = download_code # DingTalk audioContent is voice messages media_label = "voice" if att_key == "audioContent" else att_key if download_url and (self.config.include_attachments if hasattr(self.config, 'include_attachments') else True): @@ -142,6 +208,9 @@ class DingTalkChannel(Channel, WebSocketMixin, TokenMixin): else: annotations.append(f"[{media_label}: {file_name}]") + if not content and not media_paths and not annotations: + return + sender_id = payload.get("senderStaffId") or payload.get("senderId", "") conv_id = payload.get("conversationId", "") is_group = payload.get("conversationType") == "2" diff --git a/EvoScientist/channels/email/channel.py b/EvoScientist/channels/email/channel.py index f60e70d..71a4dfa 100644 --- a/EvoScientist/channels/email/channel.py +++ b/EvoScientist/channels/email/channel.py @@ -124,14 +124,24 @@ class EmailChannel(Channel, PollingMixin): attachments = [] if msg.is_multipart(): for part in msg.walk(): - content_disp = str(part.get("Content-Disposition", "")) + content_disp = part.get("Content-Disposition") or "" content_type = part.get_content_type() or "" - is_attachment = "attachment" in content_disp + is_attachment = "attachment" in content_disp.lower() is_inline_image = ( - "inline" in content_disp + "inline" in content_disp.lower() and content_type.startswith("image/") ) - if is_attachment or is_inline_image: + # Also detect non-text parts with a filename but no + # Content-Disposition header (common for PDFs, docs, + # etc. sent by some email clients). + is_named_file = ( + not is_attachment + and not is_inline_image + and part.get_filename() + and not content_type.startswith("multipart/") + and not content_type.startswith("text/") + ) + if is_attachment or is_inline_image or is_named_file: filename = part.get_filename() or "attachment" filename = _decode_hdr(filename) payload_data = part.get_payload(decode=True) diff --git a/EvoScientist/channels/wechat/channel.py b/EvoScientist/channels/wechat/channel.py index 209fd1d..a97c5d8 100644 --- a/EvoScientist/channels/wechat/channel.py +++ b/EvoScientist/channels/wechat/channel.py @@ -316,6 +316,7 @@ class WeChatChannel(Channel, WebhookMixin, TokenMixin): except Exception: return web.Response(status=400) + logger.info(f"WeChat callback POST received, body length={len(body)}") xml_data = parse_xml(body) # If encrypted, decrypt first @@ -351,6 +352,8 @@ class WeChatChannel(Channel, WebhookMixin, TokenMixin): msg_id = xml_data.get("MsgId", "") create_time = xml_data.get("CreateTime", "") + logger.info(f"WeChat message received: type={msg_type}, from={from_user}, id={msg_id}, keys={list(xml_data.keys())}") + if not from_user: return @@ -431,6 +434,21 @@ class WeChatChannel(Channel, WebhookMixin, TokenMixin): lat = xml_data.get("Location_X", "") lon = xml_data.get("Location_Y", "") text = f"[位置] {label} ({lat}, {lon})" + elif msg_type == "file": + media_id = xml_data.get("MediaId", "") + file_name = xml_data.get("Title", f"wechat_file_{msg_id}") + logger.info(f"WeChat file message: name={file_name}, media_id={media_id!r}, keys={list(xml_data.keys())}") + if media_id: + local, ann = await self._download_wechat_media( + media_id, f"wechat_file_{msg_id}_{file_name}", + ) + logger.info(f"WeChat file download result: local={local}, ann={ann}") + if local: + media_paths.append(local) + if ann: + annotations.append(ann) + if not media_paths: + annotations.append(f"[file: {file_name}]") elif msg_type == "link": title = xml_data.get("Title", "") description = xml_data.get("Description", "") diff --git a/EvoScientist/stream/events.py b/EvoScientist/stream/events.py index 97a6d9a..cc96936 100644 --- a/EvoScientist/stream/events.py +++ b/EvoScientist/stream/events.py @@ -4,6 +4,9 @@ Async generator that streams events from an agent graph, plus helpers for processing AI message chunks and tool results. """ +import base64 +import mimetypes +import os from typing import Any, AsyncIterator from langchain_core.messages import AIMessage, AIMessageChunk # type: ignore[import-untyped] @@ -64,6 +67,7 @@ async def stream_agent_events( message: str, thread_id: str, metadata: dict | None = None, + media: list[str] | None = None, ) -> AsyncIterator[dict]: """Stream events from the agent graph using async iteration. @@ -75,6 +79,7 @@ async def stream_agent_events( thread_id: Thread ID for conversation persistence metadata: Optional metadata dict merged into the LangGraph config (e.g. agent_name, updated_at for checkpoint persistence). + media: Optional list of local file paths for attachments. Yields: Event dicts: thinking, text, tool_call, tool_result, @@ -247,9 +252,41 @@ async def stream_agent_events( # 4) No real names available yet -- return generic WITHOUT caching return "sub-agent" + # Build user message content: text + inline images + file path references + user_content: str | list[dict[str, Any]] = message + if media: + _IMAGE_EXTS = frozenset({".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}) + _MAX_INLINE_SIZE = 5 * 1024 * 1024 # 5 MB + content_blocks: list[dict[str, Any]] = [] + if message: + content_blocks.append({"type": "text", "text": message}) + file_refs: list[str] = [] + for path in media: + ext = os.path.splitext(path)[1].lower() + if ext in _IMAGE_EXTS and os.path.isfile(path): + fsize = os.path.getsize(path) + if fsize <= _MAX_INLINE_SIZE: + mime = mimetypes.guess_type(path)[0] or "image/png" + with open(path, "rb") as fh: + b64 = base64.b64encode(fh.read()).decode("ascii") + content_blocks.append({"type": "image_url", "image_url": { + "url": f"data:{mime};base64,{b64}", + }}) + else: + file_refs.append(path) + else: + file_refs.append(path) + if file_refs: + ref_text = "\n".join( + f"[attached file: {os.path.basename(p)}] path: {p}" for p in file_refs + ) + content_blocks.append({"type": "text", "text": ref_text}) + if content_blocks: + user_content = content_blocks + try: async for chunk in agent.astream( - {"messages": [{"role": "user", "content": message}]}, + {"messages": [{"role": "user", "content": user_content}]}, config=config, stream_mode="messages", subgraphs=True, diff --git a/botpy.log b/botpy.log new file mode 100644 index 0000000..dd71cb8 --- /dev/null +++ b/botpy.log @@ -0,0 +1,193 @@ +2026-02-15 02:24:12,880 [INFO] (client.py:162)_bot_login [botpy] 登录机器人账号中... +2026-02-15 02:24:13,074 [INFO] (robot.py:65)update_access_token [botpy] access_token expires_in 2629 +2026-02-15 02:24:13,559 [INFO] (client.py:181)_bot_init [botpy] 程序启动... +2026-02-15 02:24:13,559 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 02:24:13,559 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 02:24:13,560 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 02:24:13,751 [INFO] (gateway.py:142)ws_identify [botpy] 鉴权中... +2026-02-15 02:24:13,885 [INFO] (gateway.py:85)on_message [botpy] 机器人「EvoSci-测试中」启动成功! +2026-02-15 02:24:13,886 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 02:30:47,988 [INFO] (client.py:162)_bot_login [botpy] 登录机器人账号中... +2026-02-15 02:30:48,188 [INFO] (robot.py:65)update_access_token [botpy] access_token expires_in 2234 +2026-02-15 02:30:48,703 [INFO] (client.py:181)_bot_init [botpy] 程序启动... +2026-02-15 02:30:48,704 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 02:30:48,704 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 02:30:48,704 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 02:30:48,877 [INFO] (gateway.py:142)ws_identify [botpy] 鉴权中... +2026-02-15 02:30:48,997 [INFO] (gateway.py:85)on_message [botpy] 机器人「EvoSci-测试中」启动成功! +2026-02-15 02:30:48,998 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 02:36:24,277 [INFO] (client.py:162)_bot_login [botpy] 登录机器人账号中... +2026-02-15 02:36:24,464 [INFO] (robot.py:65)update_access_token [botpy] access_token expires_in 1898 +2026-02-15 02:36:25,424 [INFO] (client.py:181)_bot_init [botpy] 程序启动... +2026-02-15 02:36:25,424 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 02:36:25,425 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 02:36:25,425 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 02:36:25,602 [INFO] (gateway.py:142)ws_identify [botpy] 鉴权中... +2026-02-15 02:36:25,733 [INFO] (gateway.py:85)on_message [botpy] 机器人「EvoSci-测试中」启动成功! +2026-02-15 02:36:25,733 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 03:06:25,660 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 03:06:25,660 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 03:06:30,663 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 03:06:30,663 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 03:06:30,663 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 03:06:30,839 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 03:06:30,934 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 03:06:30,934 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 03:36:30,778 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 03:36:30,780 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 03:36:35,782 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 03:36:35,782 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 03:36:35,782 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 03:36:35,972 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 03:36:36,482 [INFO] (robot.py:65)update_access_token [botpy] access_token expires_in 7200 +2026-02-15 03:36:36,573 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 03:36:36,573 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 04:06:35,954 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 04:06:35,956 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 04:06:40,958 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 04:06:40,959 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 04:06:40,959 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 04:06:41,163 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 04:06:41,281 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 04:06:41,281 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 04:36:41,169 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 04:36:41,170 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 04:36:46,172 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 04:36:46,173 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 04:36:46,173 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 04:36:46,402 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 04:36:46,499 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 04:36:46,500 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 05:06:46,456 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 05:06:46,457 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 05:06:51,460 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 05:06:51,461 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 05:06:51,461 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 05:06:51,666 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 05:06:51,774 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 05:06:51,775 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 05:36:51,710 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 05:36:51,712 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 05:36:56,715 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 05:36:56,715 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 05:36:56,715 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 05:36:56,922 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 05:36:57,135 [INFO] (robot.py:65)update_access_token [botpy] access_token expires_in 7200 +2026-02-15 05:36:57,242 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 05:36:57,242 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 06:06:56,940 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 06:06:56,942 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 06:07:01,945 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 06:07:01,945 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 06:07:01,945 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 06:07:02,134 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 06:07:02,241 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 06:07:02,241 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 06:37:02,157 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 06:37:02,158 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 06:37:07,160 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 06:37:07,160 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 06:37:07,160 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 06:37:07,377 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 06:37:07,482 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 06:37:07,483 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 07:07:07,303 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 07:07:07,305 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 07:07:12,308 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 07:07:12,308 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 07:07:12,308 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 07:07:12,494 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 07:07:12,651 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 07:07:12,651 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 07:37:12,481 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 07:37:12,483 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 07:37:17,485 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 07:37:17,486 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 07:37:17,486 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 07:37:17,722 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 07:37:17,927 [INFO] (robot.py:65)update_access_token [botpy] access_token expires_in 7200 +2026-02-15 07:37:18,044 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 07:37:18,044 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 08:07:17,716 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 08:07:17,718 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 08:07:22,721 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 08:07:22,721 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 08:07:22,722 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 08:07:22,913 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 08:07:23,017 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 08:07:23,018 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 08:37:22,911 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 08:37:22,912 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 08:37:27,914 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 08:37:27,915 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 08:37:27,915 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 08:37:28,153 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 08:37:28,263 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 08:37:28,263 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 09:07:28,149 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 09:07:28,150 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 09:07:33,152 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 09:07:33,153 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 09:07:33,153 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 09:07:33,396 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 09:07:33,515 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 09:07:33,515 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 09:37:33,390 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 09:37:33,392 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 09:37:38,394 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 09:37:38,394 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 09:37:38,394 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 09:37:38,590 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 09:37:38,807 [INFO] (robot.py:65)update_access_token [botpy] access_token expires_in 7200 +2026-02-15 09:37:38,924 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 09:37:38,924 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 10:07:38,573 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 10:07:38,575 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 10:07:43,577 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 10:07:43,578 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 10:07:43,578 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 10:07:43,746 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 10:07:43,845 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 10:07:43,845 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 10:37:43,780 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 10:37:43,782 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 10:37:48,783 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 10:37:48,783 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 10:37:48,783 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 10:37:48,961 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 10:37:49,089 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 10:37:49,089 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 11:07:48,997 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 11:07:48,999 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 11:07:54,001 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 11:07:54,002 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 11:07:54,002 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 11:07:54,244 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 11:07:54,353 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 11:07:54,353 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 11:37:54,235 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 11:37:54,236 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 11:37:59,237 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 11:37:59,238 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 11:37:59,238 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 11:37:59,430 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 11:37:59,621 [INFO] (robot.py:65)update_access_token [botpy] access_token expires_in 7200 +2026-02-15 11:37:59,732 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 11:37:59,733 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 12:07:59,439 [INFO] (gateway.py:56)on_closed [botpy] 关闭, 返回码: 4009, 返回信息: Session timed out +2026-02-15 12:07:59,440 [INFO] (gateway.py:134)ws_connect [botpy] ws关闭, 停止接收消息! +2026-02-15 12:08:04,442 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 12:08:04,443 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 12:08:04,443 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 12:08:04,630 [INFO] (gateway.py:175)ws_resume [botpy] 重连启动... +2026-02-15 12:08:04,737 [INFO] (gateway.py:90)on_message [botpy] 机器人重连成功! +2026-02-15 12:08:04,738 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动... +2026-02-15 12:33:10,772 [INFO] (client.py:162)_bot_login [botpy] 登录机器人账号中... +2026-02-15 12:33:10,925 [INFO] (robot.py:65)update_access_token [botpy] access_token expires_in 3889 +2026-02-15 12:33:11,815 [INFO] (client.py:181)_bot_init [botpy] 程序启动... +2026-02-15 12:33:11,815 [INFO] (connection.py:60)multi_run [botpy] 最大并发连接数: 1, 启动会话数: 1 +2026-02-15 12:33:11,815 [INFO] (client.py:242)bot_connect [botpy] 会话启动中... +2026-02-15 12:33:11,816 [INFO] (gateway.py:115)ws_connect [botpy] 启动中... +2026-02-15 12:33:11,999 [INFO] (gateway.py:142)ws_identify [botpy] 鉴权中... +2026-02-15 12:33:12,145 [INFO] (gateway.py:85)on_message [botpy] 机器人「EvoSci-测试中」启动成功! +2026-02-15 12:33:12,145 [INFO] (gateway.py:223)_send_heart [botpy] 心跳维持启动...