mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-19 12:47:25 +08:00
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:
@@ -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,54 +878,68 @@ class WecomBotChannel(ChatChannel):
|
|||||||
if local_path.startswith("file://"):
|
if local_path.startswith("file://"):
|
||||||
local_path = local_path[7:]
|
local_path = local_path[7:]
|
||||||
|
|
||||||
if local_path.startswith(("http://", "https://")):
|
# Temp files we create here (downloads/conversions/compressions) must be
|
||||||
try:
|
# cleaned up afterwards; the caller's original local file must not be.
|
||||||
resp = requests.get(local_path, timeout=30)
|
temp_files = []
|
||||||
resp.raise_for_status()
|
try:
|
||||||
tmp_path = f"/tmp/wecom_cb_img_{uuid.uuid4().hex[:8]}"
|
if local_path.startswith(("http://", "https://")):
|
||||||
with open(tmp_path, "wb") as f:
|
try:
|
||||||
f.write(resp.content)
|
resp = requests.get(local_path, timeout=30)
|
||||||
local_path = tmp_path
|
resp.raise_for_status()
|
||||||
except Exception as e:
|
tmp_path = f"/tmp/wecom_cb_img_{uuid.uuid4().hex[:8]}"
|
||||||
logger.error(f"[WecomBot] Failed to download image for callback reply: {e}")
|
with open(tmp_path, "wb") as f:
|
||||||
|
f.write(resp.content)
|
||||||
|
temp_files.append(tmp_path)
|
||||||
|
local_path = tmp_path
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[WecomBot] Failed to download image for callback reply: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not os.path.exists(local_path):
|
||||||
|
logger.error(f"[WecomBot] Image file not found: {local_path}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not os.path.exists(local_path):
|
formatted = self._ensure_image_format(local_path)
|
||||||
logger.error(f"[WecomBot] Image file not found: {local_path}")
|
if not formatted:
|
||||||
return None
|
return None
|
||||||
|
if formatted != local_path:
|
||||||
|
temp_files.append(formatted)
|
||||||
|
local_path = formatted
|
||||||
|
|
||||||
local_path = self._ensure_image_format(local_path)
|
# Unlike the long-connection path (which uploads and sends only a tiny
|
||||||
if not local_path:
|
# media_id), the callback reply embeds the whole image as base64 inside
|
||||||
return None
|
# an AES-encrypted body that is returned on EVERY poll. Empirically a
|
||||||
|
# ~1.5MB image (base64 ~2.1MB, encrypted ~2.8MB) makes WeCom reject the
|
||||||
|
# finish packet and poll forever, so cap well below that.
|
||||||
|
callback_max_size = 512 * 1024
|
||||||
|
if os.path.getsize(local_path) > callback_max_size:
|
||||||
|
compressed = self._compress_image(local_path, callback_max_size)
|
||||||
|
if compressed:
|
||||||
|
temp_files.append(compressed)
|
||||||
|
local_path = compressed
|
||||||
|
else:
|
||||||
|
logger.warning("[WecomBot] callback image compress failed; sending original (may be rejected)")
|
||||||
|
|
||||||
# Unlike the long-connection path (which uploads and sends only a tiny
|
try:
|
||||||
# media_id), the callback reply embeds the whole image as base64 inside
|
with open(local_path, "rb") as f:
|
||||||
# an AES-encrypted body that is returned on EVERY poll. Empirically a
|
raw = f.read()
|
||||||
# ~1.5MB image (base64 ~2.1MB, encrypted ~2.8MB) makes WeCom reject the
|
return base64.b64encode(raw).decode("utf-8"), hashlib.md5(raw).hexdigest()
|
||||||
# finish packet and poll forever, so cap well below that.
|
except Exception as e:
|
||||||
callback_max_size = 512 * 1024
|
logger.error(f"[WecomBot] Failed to read image for callback reply: {e}")
|
||||||
if os.path.getsize(local_path) > callback_max_size:
|
return None
|
||||||
compressed = self._compress_image(local_path, callback_max_size)
|
finally:
|
||||||
if compressed:
|
for path in temp_files:
|
||||||
local_path = compressed
|
try:
|
||||||
else:
|
os.remove(path)
|
||||||
logger.warning("[WecomBot] callback image compress failed; sending original (may be rejected)")
|
except OSError:
|
||||||
|
pass
|
||||||
try:
|
|
||||||
with open(local_path, "rb") as f:
|
|
||||||
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()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[WecomBot] Failed to read image for callback reply: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 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
|
||||||
|
|
||||||
|
|||||||
10
config.py
10
config.py
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user