feat(gateway): unify QQBot branding, add PLATFORM_HINTS, fix streaming, restore missing setup functions
- Rename platform from 'qq' to 'qqbot' across all integration points (Platform enum, toolset, config keys, import paths, file rename qq.py → qqbot.py) - Add PLATFORM_HINTS for QQBot in prompt_builder (QQ supports markdown) - Set SUPPORTS_MESSAGE_EDITING = False to skip streaming on QQ (prevents duplicate messages from non-editable partial + final sends) - Add _send_qqbot() standalone send function for cron/send_message tool - Add interactive _setup_qq() wizard in hermes_cli/setup.py - Restore missing _setup_signal/email/sms/dingtalk/feishu/wecom/wecom_callback functions that were lost during the original merge
This commit is contained in:
@@ -66,7 +66,7 @@ class Platform(Enum):
|
||||
WECOM_CALLBACK = "wecom_callback"
|
||||
WEIXIN = "weixin"
|
||||
BLUEBUBBLES = "bluebubbles"
|
||||
QQ = "qq"
|
||||
QQBOT = "qqbot"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -304,8 +304,8 @@ class GatewayConfig:
|
||||
# BlueBubbles uses extra dict for local server config
|
||||
elif platform == Platform.BLUEBUBBLES and config.extra.get("server_url") and config.extra.get("password"):
|
||||
connected.append(platform)
|
||||
# QQ uses extra dict for app credentials
|
||||
elif platform == Platform.QQ and config.extra.get("app_id") and config.extra.get("client_secret"):
|
||||
# QQBot uses extra dict for app credentials
|
||||
elif platform == Platform.QQBOT and config.extra.get("app_id") and config.extra.get("client_secret"):
|
||||
connected.append(platform)
|
||||
return connected
|
||||
|
||||
@@ -1117,10 +1117,10 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
qq_app_id = os.getenv("QQ_APP_ID")
|
||||
qq_client_secret = os.getenv("QQ_CLIENT_SECRET")
|
||||
if qq_app_id or qq_client_secret:
|
||||
if Platform.QQ not in config.platforms:
|
||||
config.platforms[Platform.QQ] = PlatformConfig()
|
||||
config.platforms[Platform.QQ].enabled = True
|
||||
extra = config.platforms[Platform.QQ].extra
|
||||
if Platform.QQBOT not in config.platforms:
|
||||
config.platforms[Platform.QQBOT] = PlatformConfig()
|
||||
config.platforms[Platform.QQBOT].enabled = True
|
||||
extra = config.platforms[Platform.QQBOT].extra
|
||||
if qq_app_id:
|
||||
extra["app_id"] = qq_app_id
|
||||
if qq_client_secret:
|
||||
@@ -1133,8 +1133,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||
extra["group_allow_from"] = qq_group_allowed
|
||||
qq_home = os.getenv("QQ_HOME_CHANNEL", "").strip()
|
||||
if qq_home:
|
||||
config.platforms[Platform.QQ].home_channel = HomeChannel(
|
||||
platform=Platform.QQ,
|
||||
config.platforms[Platform.QQBOT].home_channel = HomeChannel(
|
||||
platform=Platform.QQBOT,
|
||||
chat_id=qq_home,
|
||||
name=os.getenv("QQ_HOME_CHANNEL_NAME", "Home"),
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ Each adapter handles:
|
||||
"""
|
||||
|
||||
from .base import BasePlatformAdapter, MessageEvent, SendResult
|
||||
from .qq import QQAdapter
|
||||
from .qqbot import QQAdapter
|
||||
|
||||
__all__ = [
|
||||
"BasePlatformAdapter",
|
||||
|
||||
@@ -152,7 +152,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||
MAX_MESSAGE_LENGTH = MAX_MESSAGE_LENGTH
|
||||
|
||||
def __init__(self, config: PlatformConfig):
|
||||
super().__init__(config, Platform.QQ)
|
||||
super().__init__(config, Platform.QQBOT)
|
||||
|
||||
extra = config.extra or {}
|
||||
self._app_id = str(extra.get("app_id") or os.getenv("QQ_APP_ID", "")).strip()
|
||||
@@ -194,7 +194,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "QQ"
|
||||
return "QQBot"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Connection lifecycle
|
||||
@@ -658,7 +658,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except Exception:
|
||||
logger.debug("[%s] Failed to parse JSON: %r", "QQ", raw)
|
||||
logger.debug("[%s] Failed to parse JSON: %r", "QQBot", raw)
|
||||
return None
|
||||
return payload if isinstance(payload, dict) else None
|
||||
|
||||
@@ -2257,8 +2257,11 @@ class GatewayRunner:
|
||||
return None
|
||||
return BlueBubblesAdapter(config)
|
||||
|
||||
elif platform == Platform.QQ:
|
||||
from gateway.platforms.qq import QQAdapter
|
||||
elif platform == Platform.QQBOT:
|
||||
from gateway.platforms.qqbot import QQAdapter, check_qq_requirements
|
||||
if not check_qq_requirements():
|
||||
logger.warning("QQBot: aiohttp/httpx missing or QQ_APP_ID/QQ_CLIENT_SECRET not configured")
|
||||
return None
|
||||
return QQAdapter(config)
|
||||
|
||||
return None
|
||||
@@ -2302,7 +2305,7 @@ class GatewayRunner:
|
||||
Platform.WECOM_CALLBACK: "WECOM_CALLBACK_ALLOWED_USERS",
|
||||
Platform.WEIXIN: "WEIXIN_ALLOWED_USERS",
|
||||
Platform.BLUEBUBBLES: "BLUEBUBBLES_ALLOWED_USERS",
|
||||
Platform.QQ: "QQ_ALLOWED_USERS",
|
||||
Platform.QQBOT: "QQ_ALLOWED_USERS",
|
||||
}
|
||||
platform_allow_all_map = {
|
||||
Platform.TELEGRAM: "TELEGRAM_ALLOW_ALL_USERS",
|
||||
@@ -2320,7 +2323,7 @@ class GatewayRunner:
|
||||
Platform.WECOM_CALLBACK: "WECOM_CALLBACK_ALLOW_ALL_USERS",
|
||||
Platform.WEIXIN: "WEIXIN_ALLOW_ALL_USERS",
|
||||
Platform.BLUEBUBBLES: "BLUEBUBBLES_ALLOW_ALL_USERS",
|
||||
Platform.QQ: "QQ_ALLOW_ALL_USERS",
|
||||
Platform.QQBOT: "QQ_ALLOW_ALL_USERS",
|
||||
}
|
||||
|
||||
# Per-platform allow-all flag (e.g., DISCORD_ALLOW_ALL_USERS=true)
|
||||
@@ -7817,13 +7820,14 @@ class GatewayRunner:
|
||||
_adapter = self.adapters.get(source.platform)
|
||||
if _adapter:
|
||||
# Platforms that don't support editing sent messages
|
||||
# (e.g. WeChat) must not show a cursor in intermediate
|
||||
# sends — the cursor would be permanently visible because
|
||||
# it can never be edited away. Use an empty cursor for
|
||||
# such platforms so streaming still delivers the final
|
||||
# response, just without the typing indicator.
|
||||
# (e.g. QQ, WeChat) should skip streaming entirely —
|
||||
# without edit support, the consumer sends a partial
|
||||
# first message that can never be updated, resulting in
|
||||
# duplicate messages (partial + final).
|
||||
_adapter_supports_edit = getattr(_adapter, "SUPPORTS_MESSAGE_EDITING", True)
|
||||
_effective_cursor = _scfg.cursor if _adapter_supports_edit else ""
|
||||
if not _adapter_supports_edit:
|
||||
raise RuntimeError("skip streaming for non-editable platform")
|
||||
_effective_cursor = _scfg.cursor
|
||||
# Some Matrix clients render the streaming cursor
|
||||
# as a visible tofu/white-box artifact. Keep
|
||||
# streaming text on Matrix, but suppress the cursor.
|
||||
|
||||
Reference in New Issue
Block a user