mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
feat: add standalone wechat clawbot notifications
This commit is contained in:
@@ -303,6 +303,7 @@ class MoviePilotTool(BaseTool, metaclass=ABCMeta):
|
|||||||
MessageChannel.Telegram: "telegram",
|
MessageChannel.Telegram: "telegram",
|
||||||
MessageChannel.Discord: "discord",
|
MessageChannel.Discord: "discord",
|
||||||
MessageChannel.Wechat: "wechat",
|
MessageChannel.Wechat: "wechat",
|
||||||
|
MessageChannel.WechatClawBot: "wechatclawbot",
|
||||||
MessageChannel.Slack: "slack",
|
MessageChannel.Slack: "slack",
|
||||||
MessageChannel.VoceChat: "vocechat",
|
MessageChannel.VoceChat: "vocechat",
|
||||||
MessageChannel.SynologyChat: "synologychat",
|
MessageChannel.SynologyChat: "synologychat",
|
||||||
@@ -322,6 +323,7 @@ class MoviePilotTool(BaseTool, metaclass=ABCMeta):
|
|||||||
"telegram": "TELEGRAM_ADMINS",
|
"telegram": "TELEGRAM_ADMINS",
|
||||||
"discord": "DISCORD_ADMINS",
|
"discord": "DISCORD_ADMINS",
|
||||||
"wechat": "WECHAT_ADMINS",
|
"wechat": "WECHAT_ADMINS",
|
||||||
|
"wechatclawbot": "WECHATCLAWBOT_ADMINS",
|
||||||
"slack": "SLACK_ADMINS",
|
"slack": "SLACK_ADMINS",
|
||||||
"vocechat": "VOCECHAT_ADMINS",
|
"vocechat": "VOCECHAT_ADMINS",
|
||||||
"synologychat": "SYNOLOGYCHAT_ADMINS",
|
"synologychat": "SYNOLOGYCHAT_ADMINS",
|
||||||
@@ -332,6 +334,7 @@ class MoviePilotTool(BaseTool, metaclass=ABCMeta):
|
|||||||
"telegram": "TELEGRAM_CHAT_ID",
|
"telegram": "TELEGRAM_CHAT_ID",
|
||||||
"vocechat": "VOCECHAT_CHANNEL_ID",
|
"vocechat": "VOCECHAT_CHANNEL_ID",
|
||||||
"wechat": "WECHAT_BOT_CHAT_ID",
|
"wechat": "WECHAT_BOT_CHAT_ID",
|
||||||
|
"wechatclawbot": "WECHATCLAWBOT_DEFAULT_TARGET",
|
||||||
}
|
}
|
||||||
|
|
||||||
admin_key = admin_key_map.get(channel_type)
|
admin_key = admin_key_map.get(channel_type)
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ class AddSubscribeTool(MoviePilotTool):
|
|||||||
MessageChannel.Telegram: ("telegram_userid",),
|
MessageChannel.Telegram: ("telegram_userid",),
|
||||||
MessageChannel.Discord: ("discord_userid",),
|
MessageChannel.Discord: ("discord_userid",),
|
||||||
MessageChannel.Wechat: ("wechat_userid",),
|
MessageChannel.Wechat: ("wechat_userid",),
|
||||||
|
MessageChannel.WechatClawBot: ("wechatclawbot_userid",),
|
||||||
MessageChannel.Slack: ("slack_userid",),
|
MessageChannel.Slack: ("slack_userid",),
|
||||||
MessageChannel.VoceChat: ("vocechat_userid",),
|
MessageChannel.VoceChat: ("vocechat_userid",),
|
||||||
MessageChannel.SynologyChat: ("synologychat_userid",),
|
MessageChannel.SynologyChat: ("synologychat_userid",),
|
||||||
|
|||||||
+2
-1
@@ -2,7 +2,7 @@ from fastapi import APIRouter
|
|||||||
|
|
||||||
from app.api.endpoints import login, user, webhook, message, site, subscribe, \
|
from app.api.endpoints import login, user, webhook, message, site, subscribe, \
|
||||||
media, douban, search, plugin, tmdb, history, system, download, dashboard, \
|
media, douban, search, plugin, tmdb, history, system, download, dashboard, \
|
||||||
transfer, mediaserver, bangumi, storage, discover, recommend, workflow, torrent, mcp, mfa, openai, anthropic, llm
|
transfer, mediaserver, bangumi, storage, discover, recommend, workflow, torrent, mcp, mfa, openai, anthropic, llm, notification
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(login.router, prefix="/login", tags=["login"])
|
api_router.include_router(login.router, prefix="/login", tags=["login"])
|
||||||
@@ -18,6 +18,7 @@ api_router.include_router(douban.router, prefix="/douban", tags=["douban"])
|
|||||||
api_router.include_router(tmdb.router, prefix="/tmdb", tags=["tmdb"])
|
api_router.include_router(tmdb.router, prefix="/tmdb", tags=["tmdb"])
|
||||||
api_router.include_router(history.router, prefix="/history", tags=["history"])
|
api_router.include_router(history.router, prefix="/history", tags=["history"])
|
||||||
api_router.include_router(system.router, prefix="/system", tags=["system"])
|
api_router.include_router(system.router, prefix="/system", tags=["system"])
|
||||||
|
api_router.include_router(notification.router, prefix="/notification", tags=["notification"])
|
||||||
api_router.include_router(llm.router, prefix="/llm", tags=["llm"])
|
api_router.include_router(llm.router, prefix="/llm", tags=["llm"])
|
||||||
api_router.include_router(plugin.router, prefix="/plugin", tags=["plugin"])
|
api_router.include_router(plugin.router, prefix="/plugin", tags=["plugin"])
|
||||||
api_router.include_router(download.router, prefix="/download", tags=["download"])
|
api_router.include_router(download.router, prefix="/download", tags=["download"])
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from app import schemas
|
||||||
|
from app.core.module import ModuleManager
|
||||||
|
from app.db.models import User
|
||||||
|
from app.db.user_oper import get_current_active_superuser
|
||||||
|
from app.modules.wechatclawbot.wechatclawbot import WechatClawBot
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_wechatclawbot_temp_client(
|
||||||
|
source: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_BASE_URL: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_ADMINS: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None,
|
||||||
|
):
|
||||||
|
"""基于当前表单配置创建一个临时客户端,用于未保存时的扫码状态预览。"""
|
||||||
|
source_name = str(source or "").strip()
|
||||||
|
if not source_name:
|
||||||
|
return None
|
||||||
|
return WechatClawBot(
|
||||||
|
name=source_name,
|
||||||
|
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
|
||||||
|
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
|
||||||
|
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
|
||||||
|
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT,
|
||||||
|
auto_start_polling=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_wechatclawbot_client(
|
||||||
|
source: Optional[str] = None,
|
||||||
|
fallback_source: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_BASE_URL: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_ADMINS: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None,
|
||||||
|
allow_temporary: bool = False,
|
||||||
|
):
|
||||||
|
"""获取已加载的微信 ClawBot 客户端,必要时退回到临时客户端。"""
|
||||||
|
module = ModuleManager().get_running_module("WechatClawBotModule")
|
||||||
|
source_name = str(source or "").strip() or None
|
||||||
|
fallback_name = str(fallback_source or "").strip() or None
|
||||||
|
|
||||||
|
if module:
|
||||||
|
candidate_names = []
|
||||||
|
for candidate in (fallback_name, source_name):
|
||||||
|
if candidate and candidate not in candidate_names:
|
||||||
|
candidate_names.append(candidate)
|
||||||
|
|
||||||
|
if candidate_names:
|
||||||
|
for candidate in candidate_names:
|
||||||
|
config = module.get_config(candidate)
|
||||||
|
if not config:
|
||||||
|
continue
|
||||||
|
client = module.get_instance(config.name)
|
||||||
|
if client:
|
||||||
|
return client, None
|
||||||
|
else:
|
||||||
|
client = module.get_instance()
|
||||||
|
if client:
|
||||||
|
return client, None
|
||||||
|
|
||||||
|
if allow_temporary:
|
||||||
|
temp_client = _build_wechatclawbot_temp_client(
|
||||||
|
source=source_name or fallback_name,
|
||||||
|
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
|
||||||
|
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
|
||||||
|
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
|
||||||
|
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT,
|
||||||
|
)
|
||||||
|
if temp_client:
|
||||||
|
return temp_client, None
|
||||||
|
|
||||||
|
if source_name:
|
||||||
|
return None, f"未找到名为 {source_name} 的微信 ClawBot 通知配置"
|
||||||
|
return None, "微信 ClawBot 通知未启用或配置尚未保存,请先保存并启用当前渠道"
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/wechatclawbot/status",
|
||||||
|
summary="查询微信 ClawBot 登录状态",
|
||||||
|
response_model=schemas.Response,
|
||||||
|
)
|
||||||
|
def wechatclawbot_status(
|
||||||
|
source: Optional[str] = None,
|
||||||
|
fallback_source: Optional[str] = None,
|
||||||
|
refresh_remote: bool = True,
|
||||||
|
auto_generate_qrcode: bool = True,
|
||||||
|
WECHATCLAWBOT_BASE_URL: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_ADMINS: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None,
|
||||||
|
_: User = Depends(get_current_active_superuser),
|
||||||
|
):
|
||||||
|
"""查询微信 ClawBot 登录状态和二维码。"""
|
||||||
|
client, errmsg = _get_wechatclawbot_client(
|
||||||
|
source=source,
|
||||||
|
fallback_source=fallback_source,
|
||||||
|
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
|
||||||
|
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
|
||||||
|
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
|
||||||
|
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT,
|
||||||
|
allow_temporary=True,
|
||||||
|
)
|
||||||
|
if not client:
|
||||||
|
return schemas.Response(success=False, message=errmsg)
|
||||||
|
return schemas.Response(
|
||||||
|
success=True,
|
||||||
|
data=client.get_status(
|
||||||
|
refresh_remote=refresh_remote,
|
||||||
|
auto_generate_qrcode=auto_generate_qrcode,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/wechatclawbot/refresh",
|
||||||
|
summary="刷新微信 ClawBot 二维码",
|
||||||
|
response_model=schemas.Response,
|
||||||
|
)
|
||||||
|
def refresh_wechatclawbot_qrcode(
|
||||||
|
source: Optional[str] = None,
|
||||||
|
fallback_source: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_BASE_URL: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_ADMINS: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None,
|
||||||
|
_: User = Depends(get_current_active_superuser),
|
||||||
|
):
|
||||||
|
"""刷新微信 ClawBot 二维码。"""
|
||||||
|
client, errmsg = _get_wechatclawbot_client(
|
||||||
|
source=source,
|
||||||
|
fallback_source=fallback_source,
|
||||||
|
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
|
||||||
|
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
|
||||||
|
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
|
||||||
|
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT,
|
||||||
|
allow_temporary=True,
|
||||||
|
)
|
||||||
|
if not client:
|
||||||
|
return schemas.Response(success=False, message=errmsg)
|
||||||
|
result = client.refresh_qrcode()
|
||||||
|
return schemas.Response(
|
||||||
|
success=bool(result.get("success")),
|
||||||
|
message=result.get("message"),
|
||||||
|
data=result,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/wechatclawbot/logout",
|
||||||
|
summary="退出微信 ClawBot 登录",
|
||||||
|
response_model=schemas.Response,
|
||||||
|
)
|
||||||
|
def logout_wechatclawbot(
|
||||||
|
source: Optional[str] = None,
|
||||||
|
fallback_source: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_BASE_URL: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_ADMINS: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None,
|
||||||
|
_: User = Depends(get_current_active_superuser),
|
||||||
|
):
|
||||||
|
"""退出微信 ClawBot 登录。"""
|
||||||
|
client, errmsg = _get_wechatclawbot_client(
|
||||||
|
source=source,
|
||||||
|
fallback_source=fallback_source,
|
||||||
|
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
|
||||||
|
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
|
||||||
|
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
|
||||||
|
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT,
|
||||||
|
allow_temporary=True,
|
||||||
|
)
|
||||||
|
if not client:
|
||||||
|
return schemas.Response(success=False, message=errmsg)
|
||||||
|
result = client.logout()
|
||||||
|
return schemas.Response(
|
||||||
|
success=bool(result.get("success")),
|
||||||
|
message=result.get("message"),
|
||||||
|
data=result,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/wechatclawbot/test",
|
||||||
|
summary="测试微信 ClawBot 连通性",
|
||||||
|
response_model=schemas.Response,
|
||||||
|
)
|
||||||
|
def test_wechatclawbot(
|
||||||
|
source: Optional[str] = None,
|
||||||
|
fallback_source: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_BASE_URL: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_ADMINS: Optional[str] = None,
|
||||||
|
WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None,
|
||||||
|
_: User = Depends(get_current_active_superuser),
|
||||||
|
):
|
||||||
|
"""测试微信 ClawBot 当前登录态是否可用。"""
|
||||||
|
client, errmsg = _get_wechatclawbot_client(
|
||||||
|
source=source,
|
||||||
|
fallback_source=fallback_source,
|
||||||
|
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
|
||||||
|
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
|
||||||
|
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
|
||||||
|
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT,
|
||||||
|
allow_temporary=True,
|
||||||
|
)
|
||||||
|
if not client:
|
||||||
|
return schemas.Response(success=False, message=errmsg)
|
||||||
|
state, message = client.test_connection()
|
||||||
|
return schemas.Response(success=state, message=message)
|
||||||
@@ -1139,6 +1139,15 @@ class MessageChain(ChainBase):
|
|||||||
source=source,
|
source=source,
|
||||||
)
|
)
|
||||||
filename = "input.amr"
|
filename = "input.amr"
|
||||||
|
elif audio_ref.startswith("wxclaw://voice/"):
|
||||||
|
content = self.run_module(
|
||||||
|
"download_wechat_media_bytes",
|
||||||
|
media_ref=audio_ref,
|
||||||
|
source=source,
|
||||||
|
)
|
||||||
|
filename = self._guess_audio_filename(
|
||||||
|
audio_ref, default="input.amr"
|
||||||
|
)
|
||||||
elif audio_ref.startswith("slack://file/"):
|
elif audio_ref.startswith("slack://file/"):
|
||||||
content = self.run_module(
|
content = self.run_module(
|
||||||
"download_slack_file_bytes", file_ref=audio_ref, source=source
|
"download_slack_file_bytes", file_ref=audio_ref, source=source
|
||||||
@@ -1270,6 +1279,8 @@ class MessageChain(ChainBase):
|
|||||||
"wxwork://media_id/"
|
"wxwork://media_id/"
|
||||||
) or attachment_ref.startswith(
|
) or attachment_ref.startswith(
|
||||||
"wxbot://image/"
|
"wxbot://image/"
|
||||||
|
) or attachment_ref.startswith(
|
||||||
|
"wxclaw://image/"
|
||||||
):
|
):
|
||||||
data_url = self.run_module(
|
data_url = self.run_module(
|
||||||
"download_wechat_image_to_data_url",
|
"download_wechat_image_to_data_url",
|
||||||
@@ -1438,10 +1449,19 @@ class MessageChain(ChainBase):
|
|||||||
"download_wechat_image_to_data_url", image_ref=file_ref, source=source
|
"download_wechat_image_to_data_url", image_ref=file_ref, source=source
|
||||||
)
|
)
|
||||||
return self._decode_data_url_bytes(data_url) if data_url else None
|
return self._decode_data_url_bytes(data_url) if data_url else None
|
||||||
|
if file_ref.startswith("wxclaw://image/"):
|
||||||
|
data_url = self.run_module(
|
||||||
|
"download_wechat_image_to_data_url", image_ref=file_ref, source=source
|
||||||
|
)
|
||||||
|
return self._decode_data_url_bytes(data_url) if data_url else None
|
||||||
if file_ref.startswith("wxbot://file/"):
|
if file_ref.startswith("wxbot://file/"):
|
||||||
file_url = unquote(file_ref.replace("wxbot://file/", "", 1))
|
file_url = unquote(file_ref.replace("wxbot://file/", "", 1))
|
||||||
resp = RequestUtils(timeout=30).get_res(file_url)
|
resp = RequestUtils(timeout=30).get_res(file_url)
|
||||||
return resp.content if resp and resp.content else None
|
return resp.content if resp and resp.content else None
|
||||||
|
if file_ref.startswith("wxclaw://file/") or file_ref.startswith("wxclaw://voice/"):
|
||||||
|
return self.run_module(
|
||||||
|
"download_wechat_media_bytes", media_ref=file_ref, source=source
|
||||||
|
)
|
||||||
if file_ref.startswith("slack://file/"):
|
if file_ref.startswith("slack://file/"):
|
||||||
return self.run_module(
|
return self.run_module(
|
||||||
"download_slack_file_bytes", file_ref=file_ref, source=source
|
"download_slack_file_bytes", file_ref=file_ref, source=source
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class WechatModule(_ModuleBase, _MessageBase[WeChat]):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_name() -> str:
|
def get_name() -> str:
|
||||||
return "微信"
|
return "企业微信"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_type() -> ModuleType:
|
def get_type() -> ModuleType:
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
import json
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from app.core.context import Context, MediaInfo
|
||||||
|
from app.log import logger
|
||||||
|
from app.modules import _MessageBase, _ModuleBase
|
||||||
|
from app.modules.wechatclawbot.wechatclawbot import WechatClawBot
|
||||||
|
from app.schemas import CommingMessage, Notification
|
||||||
|
from app.schemas.types import MessageChannel, ModuleType
|
||||||
|
|
||||||
|
|
||||||
|
class WechatClawBotModule(_ModuleBase, _MessageBase[WechatClawBot]):
|
||||||
|
def init_module(self) -> None:
|
||||||
|
"""初始化模块。"""
|
||||||
|
self.stop()
|
||||||
|
super().init_service(
|
||||||
|
service_name=WechatClawBot.__name__.lower(), service_type=WechatClawBot
|
||||||
|
)
|
||||||
|
self._channel = MessageChannel.WechatClawBot
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "微信 ClawBot"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_type() -> ModuleType:
|
||||||
|
"""获取模块类型。"""
|
||||||
|
return ModuleType.Notification
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subtype() -> MessageChannel:
|
||||||
|
"""获取模块子类型。"""
|
||||||
|
return MessageChannel.WechatClawBot
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_priority() -> int:
|
||||||
|
"""获取模块优先级。"""
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""停止模块。"""
|
||||||
|
for client in self.get_instances().values():
|
||||||
|
if hasattr(client, "stop"):
|
||||||
|
try:
|
||||||
|
client.stop()
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(f"停止微信 ClawBot 模块实例失败:{err}")
|
||||||
|
|
||||||
|
def test(self) -> Optional[Tuple[bool, str]]:
|
||||||
|
"""测试模块连接性。"""
|
||||||
|
if not self.get_instances():
|
||||||
|
return None
|
||||||
|
for name, client in self.get_instances().items():
|
||||||
|
state, message = client.test_connection()
|
||||||
|
if not state:
|
||||||
|
return False, f"微信 ClawBot {name} 未就绪:{message}"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_json(body: Any) -> Optional[dict]:
|
||||||
|
if isinstance(body, dict):
|
||||||
|
payload = body
|
||||||
|
elif isinstance(body, bytes):
|
||||||
|
payload = json.loads(body.decode("utf-8", errors="ignore"))
|
||||||
|
else:
|
||||||
|
payload = json.loads(body)
|
||||||
|
while isinstance(payload, str):
|
||||||
|
payload = json.loads(payload)
|
||||||
|
return payload if isinstance(payload, dict) else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_audio_refs(audio_refs: Any) -> Optional[List[str]]:
|
||||||
|
if not audio_refs:
|
||||||
|
return None
|
||||||
|
if not isinstance(audio_refs, list):
|
||||||
|
audio_refs = [audio_refs]
|
||||||
|
normalized = [str(item).strip() for item in audio_refs if str(item).strip()]
|
||||||
|
return normalized or None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_files(files: Any) -> Optional[List[CommingMessage.MessageAttachment]]:
|
||||||
|
if not files:
|
||||||
|
return None
|
||||||
|
if not isinstance(files, list):
|
||||||
|
files = [files]
|
||||||
|
normalized = []
|
||||||
|
for item in files:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
ref = item.get("ref") or item.get("url") or item.get("file_url")
|
||||||
|
if not ref:
|
||||||
|
continue
|
||||||
|
size = item.get("size")
|
||||||
|
try:
|
||||||
|
size = int(size) if size is not None else None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
size = None
|
||||||
|
normalized.append(
|
||||||
|
CommingMessage.MessageAttachment(
|
||||||
|
ref=ref,
|
||||||
|
name=item.get("name") or item.get("filename"),
|
||||||
|
mime_type=item.get("mime_type") or item.get("content_type"),
|
||||||
|
size=size,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return normalized or None
|
||||||
|
|
||||||
|
def message_parser(
|
||||||
|
self, source: str, body: Any, form: Any, args: Any
|
||||||
|
) -> Optional[CommingMessage]:
|
||||||
|
"""解析微信 ClawBot 转发到消息入口的 JSON 报文。"""
|
||||||
|
client_config = self.get_config(source)
|
||||||
|
if not client_config:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
message = self._load_json(body)
|
||||||
|
except Exception as err:
|
||||||
|
logger.debug(f"解析微信 ClawBot 消息失败:{err}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
return None
|
||||||
|
channel_name = (message.get("__channel__") or "").strip().lower()
|
||||||
|
if channel_name and channel_name != "wechatclawbot":
|
||||||
|
return None
|
||||||
|
|
||||||
|
user_id = str(message.get("userid") or "").strip()
|
||||||
|
if not user_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
text = str(message.get("text") or "").strip()
|
||||||
|
username = str(message.get("username") or user_id).strip() or user_id
|
||||||
|
images = CommingMessage.MessageImage.normalize_list(message.get("images"))
|
||||||
|
audio_refs = self._normalize_audio_refs(message.get("audio_refs"))
|
||||||
|
files = self._normalize_files(message.get("files"))
|
||||||
|
if not text and not images and not audio_refs and not files:
|
||||||
|
return None
|
||||||
|
|
||||||
|
admins = [
|
||||||
|
admin.strip()
|
||||||
|
for admin in str(client_config.config.get("WECHATCLAWBOT_ADMINS") or "").split(",")
|
||||||
|
if admin.strip()
|
||||||
|
]
|
||||||
|
if text.startswith("/") and admins and user_id not in admins:
|
||||||
|
client = self.get_instance(client_config.name)
|
||||||
|
if client:
|
||||||
|
client.send_msg(title="只有管理员才有权限执行此命令", userid=user_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"收到来自 {client_config.name} 的微信 ClawBot 消息:"
|
||||||
|
f"userid={user_id}, text={text}, images={len(images) if images else 0}, "
|
||||||
|
f"audios={len(audio_refs) if audio_refs else 0}, files={len(files) if files else 0}"
|
||||||
|
)
|
||||||
|
return CommingMessage(
|
||||||
|
channel=MessageChannel.WechatClawBot,
|
||||||
|
source=client_config.name,
|
||||||
|
userid=user_id,
|
||||||
|
username=username,
|
||||||
|
text=text,
|
||||||
|
message_id=message.get("message_id"),
|
||||||
|
chat_id=str(message.get("chat_id") or "") or None,
|
||||||
|
images=images,
|
||||||
|
audio_refs=audio_refs,
|
||||||
|
files=files,
|
||||||
|
)
|
||||||
|
|
||||||
|
def post_message(self, message: Notification, **kwargs) -> None:
|
||||||
|
"""发送消息。"""
|
||||||
|
for conf in self.get_configs().values():
|
||||||
|
if not self.check_message(message, conf.name):
|
||||||
|
continue
|
||||||
|
targets = message.targets
|
||||||
|
userid = message.userid
|
||||||
|
if not userid and targets is not None:
|
||||||
|
userid = targets.get("wechatclawbot_userid")
|
||||||
|
if not userid:
|
||||||
|
logger.warning("用户没有指定 微信 ClawBot 用户ID,消息无法发送")
|
||||||
|
return
|
||||||
|
client: WechatClawBot = self.get_instance(conf.name)
|
||||||
|
if not client:
|
||||||
|
continue
|
||||||
|
if message.file_path:
|
||||||
|
client.send_file(
|
||||||
|
file_path=message.file_path,
|
||||||
|
file_name=message.file_name,
|
||||||
|
title=message.title,
|
||||||
|
text=message.text,
|
||||||
|
userid=userid,
|
||||||
|
)
|
||||||
|
elif message.voice_path:
|
||||||
|
client.send_file(
|
||||||
|
file_path=message.voice_path,
|
||||||
|
title=message.voice_caption or message.title,
|
||||||
|
text=message.text,
|
||||||
|
userid=userid,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
client.send_msg(
|
||||||
|
title=message.title or "",
|
||||||
|
text=message.text,
|
||||||
|
image=message.image,
|
||||||
|
userid=userid,
|
||||||
|
link=message.link,
|
||||||
|
)
|
||||||
|
|
||||||
|
def download_wechat_image_to_data_url(
|
||||||
|
self, image_ref: str, source: str
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""下载微信 ClawBot 图片并转换为 data URL。"""
|
||||||
|
if not image_ref or not image_ref.startswith("wxclaw://image/"):
|
||||||
|
return None
|
||||||
|
client_config = self.get_config(source)
|
||||||
|
if not client_config:
|
||||||
|
return None
|
||||||
|
client = self.get_instance(client_config.name)
|
||||||
|
if not client:
|
||||||
|
return None
|
||||||
|
return client.download_image_to_data_url(image_ref)
|
||||||
|
|
||||||
|
def download_wechat_media_bytes(
|
||||||
|
self, media_ref: str, source: str
|
||||||
|
) -> Optional[bytes]:
|
||||||
|
"""下载微信 ClawBot 语音或文件附件。"""
|
||||||
|
if not media_ref or not media_ref.startswith(("wxclaw://file/", "wxclaw://voice/")):
|
||||||
|
return None
|
||||||
|
client_config = self.get_config(source)
|
||||||
|
if not client_config:
|
||||||
|
return None
|
||||||
|
client = self.get_instance(client_config.name)
|
||||||
|
if not client:
|
||||||
|
return None
|
||||||
|
return client.download_media_bytes(media_ref)
|
||||||
|
|
||||||
|
def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> None:
|
||||||
|
"""发送媒体选择列表。"""
|
||||||
|
for conf in self.get_configs().values():
|
||||||
|
if not self.check_message(message, conf.name):
|
||||||
|
continue
|
||||||
|
client: WechatClawBot = self.get_instance(conf.name)
|
||||||
|
if client:
|
||||||
|
client.send_medias_msg(medias=medias, userid=message.userid)
|
||||||
|
|
||||||
|
def post_torrents_message(self, message: Notification, torrents: List[Context]) -> None:
|
||||||
|
"""发送种子选择列表。"""
|
||||||
|
for conf in self.get_configs().values():
|
||||||
|
if not self.check_message(message, conf.name):
|
||||||
|
continue
|
||||||
|
client: WechatClawBot = self.get_instance(conf.name)
|
||||||
|
if client:
|
||||||
|
client.send_torrents_msg(
|
||||||
|
torrents=torrents,
|
||||||
|
userid=message.userid,
|
||||||
|
title=message.title,
|
||||||
|
link=message.link,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register_commands(self, commands: Dict[str, dict]):
|
||||||
|
"""微信 ClawBot 不支持原生菜单命令,统一走文本交互。"""
|
||||||
|
logger.debug("微信 ClawBot 不支持原生菜单命令,跳过命令注册")
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -324,6 +324,17 @@ class ChannelCapabilityManager:
|
|||||||
},
|
},
|
||||||
fallback_enabled=True,
|
fallback_enabled=True,
|
||||||
),
|
),
|
||||||
|
MessageChannel.WechatClawBot: ChannelCapabilities(
|
||||||
|
channel=MessageChannel.WechatClawBot,
|
||||||
|
capabilities={
|
||||||
|
ChannelCapability.MARKDOWN,
|
||||||
|
ChannelCapability.IMAGES,
|
||||||
|
ChannelCapability.LINKS,
|
||||||
|
ChannelCapability.FILE_SENDING,
|
||||||
|
},
|
||||||
|
max_message_length=2800,
|
||||||
|
fallback_enabled=True,
|
||||||
|
),
|
||||||
MessageChannel.Slack: ChannelCapabilities(
|
MessageChannel.Slack: ChannelCapabilities(
|
||||||
channel=MessageChannel.Slack,
|
channel=MessageChannel.Slack,
|
||||||
capabilities={
|
capabilities={
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ class MessageChannel(Enum):
|
|||||||
消息渠道
|
消息渠道
|
||||||
"""
|
"""
|
||||||
Wechat = "微信"
|
Wechat = "微信"
|
||||||
|
WechatClawBot = "微信ClawBot"
|
||||||
Telegram = "Telegram"
|
Telegram = "Telegram"
|
||||||
Slack = "Slack"
|
Slack = "Slack"
|
||||||
Discord = "Discord"
|
Discord = "Discord"
|
||||||
|
|||||||
Reference in New Issue
Block a user