refactor(wecom_bot): use wecom_bot_mode field and clean up temp images

Replace the boolean wecom_bot_callback with a wecom_bot_mode field
("websocket" | "webhook"), consistent with the Feishu channel's
feishu_event_mode. Update startup() and send() to branch on the mode.

Also fix a temp-file leak in _load_image_base64: downloads, format
conversions and compressions wrote to /tmp but were never removed. Track
only the temp files created here and delete them in a finally block,
leaving the caller's original local file untouched.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
6vision
2026-06-16 17:23:31 +08:00
parent 52209217fc
commit eed2eab014
2 changed files with 61 additions and 47 deletions

View File

@@ -103,8 +103,8 @@ class WecomBotChannel(ChatChannel):
self._pending_lock = threading.Lock() self._pending_lock = threading.Lock()
self._stream_states = {} # req_id -> {"stream_id": str, "content": str} self._stream_states = {} # req_id -> {"stream_id": str, "content": str}
# Callback (webhook) mode state # Transport mode: "websocket" (long connection) or "webhook" (HTTP callback)
self.callback_mode = False self.mode = "websocket"
self._crypt = None self._crypt = None
self._http_server = None self._http_server = None
# stream_id -> {"committed", "current", "finished", "images", "last_access"} # stream_id -> {"committed", "current", "finished", "images", "last_access"}
@@ -119,8 +119,8 @@ class WecomBotChannel(ChatChannel):
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def startup(self): def startup(self):
self.callback_mode = bool(conf().get("wecom_bot_callback", False)) self.mode = conf().get("wecom_bot_mode", "websocket")
if self.callback_mode: if self.mode == "webhook":
self._startup_callback() self._startup_callback()
return return
@@ -878,6 +878,10 @@ class WecomBotChannel(ChatChannel):
if local_path.startswith("file://"): if local_path.startswith("file://"):
local_path = local_path[7:] local_path = local_path[7:]
# Temp files we create here (downloads/conversions/compressions) must be
# cleaned up afterwards; the caller's original local file must not be.
temp_files = []
try:
if local_path.startswith(("http://", "https://")): if local_path.startswith(("http://", "https://")):
try: try:
resp = requests.get(local_path, timeout=30) resp = requests.get(local_path, timeout=30)
@@ -885,6 +889,7 @@ class WecomBotChannel(ChatChannel):
tmp_path = f"/tmp/wecom_cb_img_{uuid.uuid4().hex[:8]}" tmp_path = f"/tmp/wecom_cb_img_{uuid.uuid4().hex[:8]}"
with open(tmp_path, "wb") as f: with open(tmp_path, "wb") as f:
f.write(resp.content) f.write(resp.content)
temp_files.append(tmp_path)
local_path = tmp_path local_path = tmp_path
except Exception as e: except Exception as e:
logger.error(f"[WecomBot] Failed to download image for callback reply: {e}") logger.error(f"[WecomBot] Failed to download image for callback reply: {e}")
@@ -894,9 +899,12 @@ class WecomBotChannel(ChatChannel):
logger.error(f"[WecomBot] Image file not found: {local_path}") logger.error(f"[WecomBot] Image file not found: {local_path}")
return None return None
local_path = self._ensure_image_format(local_path) formatted = self._ensure_image_format(local_path)
if not local_path: if not formatted:
return None return None
if formatted != local_path:
temp_files.append(formatted)
local_path = formatted
# Unlike the long-connection path (which uploads and sends only a tiny # Unlike the long-connection path (which uploads and sends only a tiny
# media_id), the callback reply embeds the whole image as base64 inside # media_id), the callback reply embeds the whole image as base64 inside
@@ -907,6 +915,7 @@ class WecomBotChannel(ChatChannel):
if os.path.getsize(local_path) > callback_max_size: if os.path.getsize(local_path) > callback_max_size:
compressed = self._compress_image(local_path, callback_max_size) compressed = self._compress_image(local_path, callback_max_size)
if compressed: if compressed:
temp_files.append(compressed)
local_path = compressed local_path = compressed
else: else:
logger.warning("[WecomBot] callback image compress failed; sending original (may be rejected)") logger.warning("[WecomBot] callback image compress failed; sending original (may be rejected)")
@@ -914,18 +923,23 @@ class WecomBotChannel(ChatChannel):
try: try:
with open(local_path, "rb") as f: with open(local_path, "rb") as f:
raw = f.read() raw = f.read()
logger.debug(f"[WecomBot] callback image base64 ready: raw={len(raw)} bytes")
return base64.b64encode(raw).decode("utf-8"), hashlib.md5(raw).hexdigest() return base64.b64encode(raw).decode("utf-8"), hashlib.md5(raw).hexdigest()
except Exception as e: except Exception as e:
logger.error(f"[WecomBot] Failed to read image for callback reply: {e}") logger.error(f"[WecomBot] Failed to read image for callback reply: {e}")
return None return None
finally:
for path in temp_files:
try:
os.remove(path)
except OSError:
pass
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Send reply # Send reply
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def send(self, reply: Reply, context: Context): def send(self, reply: Reply, context: Context):
if self.callback_mode: if self.mode == "webhook":
self._callback_send(reply, context) self._callback_send(reply, context)
return return

View File

@@ -180,11 +180,11 @@ available_setting = {
# WeCom smart bot config (long connection mode) # WeCom smart bot config (long connection mode)
"wecom_bot_id": "", # WeCom smart bot BotID "wecom_bot_id": "", # WeCom smart bot BotID
"wecom_bot_secret": "", # WeCom smart bot long-connection secret "wecom_bot_secret": "", # WeCom smart bot long-connection secret
# WeCom smart bot callback (webhook) mode; default off, keep using the long connection # WeCom smart bot transport mode: "websocket" (long connection) or "webhook" (HTTP callback)
"wecom_bot_callback": False, # whether to receive messages via HTTP callback (webhook) instead of the long connection "wecom_bot_mode": "websocket",
"wecom_bot_token": "", # callback mode: Token configured on the bot's receive-message URL "wecom_bot_token": "", # webhook mode: Token configured on the bot's receive-message URL
"wecom_bot_encoding_aes_key": "", # callback mode: EncodingAESKey configured on the bot's receive-message URL "wecom_bot_encoding_aes_key": "", # webhook mode: EncodingAESKey configured on the bot's receive-message URL
"wecom_bot_port": 9892, # callback mode: local HTTP server port for the receive-message URL "wecom_bot_port": 9892, # webhook mode: local HTTP server port for the receive-message URL
# Telegram config # Telegram config
"telegram_token": "", # Bot token from @BotFather "telegram_token": "", # Bot token from @BotFather
"telegram_proxy": "", # Optional HTTP/SOCKS5 proxy, e.g. http://127.0.0.1:7890 or socks5://127.0.0.1:1080 (empty falls back to env vars) "telegram_proxy": "", # Optional HTTP/SOCKS5 proxy, e.g. http://127.0.0.1:7890 or socks5://127.0.0.1:1080 (empty falls back to env vars)