Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions astrbot/core/platform/sources/wecom/wecom_adapter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import os
import sys
import time
import uuid
from collections.abc import Awaitable, Callable
from typing import Any, cast
Expand Down Expand Up @@ -140,6 +141,8 @@ async def shutdown_trigger(self) -> None:

@register_platform_adapter("wecom", "wecom 适配器", support_streaming_message=False)
class WecomPlatformAdapter(Platform):
WECHAT_KF_TEXT_CONTENT_DEDUP_TTL_SECONDS = 15

def __init__(
self,
platform_config: dict,
Expand All @@ -166,6 +169,7 @@ def __init__(

self.server = WecomServer(self._event_queue, self.config)
self.agent_id: str | None = None
self._wechat_kf_seen_text_messages: dict[str, float] = {}

self.client = WeChatClient(
self.config["corpid"].strip(),
Expand Down Expand Up @@ -210,6 +214,28 @@ def get_latest_msg_item() -> dict | None:

self.server.callback = callback

def _is_duplicate_wechat_kf_text_message(self, session_id: str, text: str) -> bool:
normalized_text = text.strip()
if not normalized_text:
return False

now = time.monotonic()
expired_keys = [
key
for key, expires_at in self._wechat_kf_seen_text_messages.items()
if expires_at <= now
]
for key in expired_keys:
self._wechat_kf_seen_text_messages.pop(key, None)

dedup_key = f"{session_id}:{normalized_text}"
if dedup_key in self._wechat_kf_seen_text_messages:
return True
self._wechat_kf_seen_text_messages[dedup_key] = (
now + self.WECHAT_KF_TEXT_CONTENT_DEDUP_TTL_SECONDS
)
return False

@override
async def send_by_session(
self,
Expand Down Expand Up @@ -390,6 +416,13 @@ async def convert_wechat_kf_message(self, msg: dict) -> AstrBotMessage | None:
abm.message_str = ""
if msgtype == "text":
text = msg.get("text", {}).get("content", "").strip()
if self._is_duplicate_wechat_kf_text_message(abm.session_id, text):
logger.debug(
"忽略 15 秒内重复微信客服文本消息 session_id=%s text=%s",
abm.session_id,
text,
)
return None
abm.message = [Plain(text=text)]
abm.message_str = text
elif msgtype == "image":
Expand Down
20 changes: 17 additions & 3 deletions astrbot/core/tools/computer_tools/fs.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from astrbot.core.astr_agent_context import AstrAgentContext
from astrbot.core.computer.computer_client import get_booter
from astrbot.core.computer.file_read_utils import read_file_tool_result
from astrbot.core.message.components import File
from astrbot.core.message.components import File, Image
from astrbot.core.utils.astrbot_path import (
get_astrbot_skills_path,
get_astrbot_system_tmp_path,
Expand All @@ -64,6 +64,7 @@
_SANDBOX_RUNTIME_TOOL_CONFIG = {
"provider_settings.computer_use_runtime": "sandbox",
}
_IMAGE_FILE_SUFFIXES = {".bmp", ".gif", ".jpeg", ".jpg", ".png", ".webp"}


def _restricted_env_path_labels(umo: str) -> list[str]:
Expand Down Expand Up @@ -729,19 +730,32 @@ async def call(
if also_send_to_user:
try:
name = os.path.basename(local_path)
if Path(local_path).suffix.lower() in _IMAGE_FILE_SUFFIXES:
message_component = Image.fromFileSystem(local_path)
sent_as = "image"
else:
message_component = File(name=name, file=local_path)
sent_as = "file"
await context.context.event.send(
MessageChain(chain=[File(name=name, file=local_path)])
MessageChain(chain=[message_component])
)
except Exception as e:
logger.error(f"Error sending file message: {e}")
return (
f"File downloaded successfully to {local_path} "
f"but sending to user failed: {e}"
)

# remove
# try:
# os.remove(local_path)
# except Exception as e:
# logger.error(f"Error removing temp file {local_path}: {e}")

return f"File downloaded successfully to {local_path} and sent to user."
return (
f"File downloaded successfully to {local_path} "
f"and sent to user as {sent_as}."
)

return f"File downloaded successfully to {local_path}"
except Exception as e:
Expand Down
15 changes: 8 additions & 7 deletions astrbot/core/utils/network_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import httpx

from astrbot import logger
from astrbot.utils.http_ssl_common import build_ssl_context_with_certifi

_SYSTEM_SSL_CTX = ssl.create_default_context()
_SYSTEM_SSL_CTX = build_ssl_context_with_certifi()


def is_connection_error(exc: BaseException) -> bool:
Expand Down Expand Up @@ -92,9 +93,9 @@ def create_proxy_client(
) -> httpx.AsyncClient:
"""Create an httpx AsyncClient with proxy configuration if provided.

Uses the system SSL certificate store instead of certifi, which avoids
SSL verification failures for endpoints whose CA chain is not in certifi
but is trusted by the operating system.
Uses a hybrid SSL context that combines the system SSL certificate store
with certifi as a fallback, ensuring compatibility across different
environments including Windows where the system store may be incomplete.

Note: The caller is responsible for closing the client when done.
Consider using the client as a context manager or calling aclose() explicitly.
Expand All @@ -103,11 +104,11 @@ def create_proxy_client(
provider_label: The provider name for log prefix (e.g., "OpenAI", "Gemini")
proxy: The proxy address (e.g., "http://127.0.0.1:7890"), or None/empty
headers: Optional custom headers to include in every request
verify: Optional override for TLS verification. Defaults to the shared
system SSL context when not provided.
verify: Optional override for TLS verification. Defaults to the hybrid
SSL context (system store + certifi) when not provided.

Returns:
An httpx.AsyncClient created with the shared system SSL context; the proxy is applied only if one is provided.
An httpx.AsyncClient created with the hybrid SSL context (system store + certifi); the proxy is applied only if one is provided.
"""
resolved_verify = _SYSTEM_SSL_CTX if verify is None else verify
if proxy:
Expand Down
Loading