mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
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:
parent
87bfc28e70
commit
884cd920d4
20 changed files with 176 additions and 113 deletions
|
|
@ -55,7 +55,7 @@ hermes-agent/
|
||||||
├── gateway/ # Messaging platform gateway
|
├── gateway/ # Messaging platform gateway
|
||||||
│ ├── run.py # Main loop, slash commands, message dispatch
|
│ ├── run.py # Main loop, slash commands, message dispatch
|
||||||
│ ├── session.py # SessionStore — conversation persistence
|
│ ├── session.py # SessionStore — conversation persistence
|
||||||
│ └── platforms/ # Adapters: telegram, discord, slack, whatsapp, homeassistant, signal
|
│ └── platforms/ # Adapters: telegram, discord, slack, whatsapp, homeassistant, signal, qqbot
|
||||||
├── acp_adapter/ # ACP server (VS Code / Zed / JetBrains integration)
|
├── acp_adapter/ # ACP server (VS Code / Zed / JetBrains integration)
|
||||||
├── cron/ # Scheduler (jobs.py, scheduler.py)
|
├── cron/ # Scheduler (jobs.py, scheduler.py)
|
||||||
├── environments/ # RL training environments (Atropos)
|
├── environments/ # RL training environments (Atropos)
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,12 @@ PLATFORM_HINTS = {
|
||||||
"downloaded and sent as native photos. Do NOT tell the user you lack file-sending "
|
"downloaded and sent as native photos. Do NOT tell the user you lack file-sending "
|
||||||
"capability — use MEDIA: syntax whenever a file delivery is appropriate."
|
"capability — use MEDIA: syntax whenever a file delivery is appropriate."
|
||||||
),
|
),
|
||||||
|
"qqbot": (
|
||||||
|
"You are on QQ, a popular Chinese messaging platform. QQ supports markdown formatting "
|
||||||
|
"and emoji. You can send media files natively: include MEDIA:/absolute/path/to/file in "
|
||||||
|
"your response. Images are sent as native photos, and other files arrive as downloadable "
|
||||||
|
"documents."
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -523,7 +523,7 @@ agent:
|
||||||
# - A preset like "hermes-cli" or "hermes-telegram" (curated tool set)
|
# - A preset like "hermes-cli" or "hermes-telegram" (curated tool set)
|
||||||
# - A list of individual toolsets to compose your own (see list below)
|
# - A list of individual toolsets to compose your own (see list below)
|
||||||
#
|
#
|
||||||
# Supported platform keys: cli, telegram, discord, whatsapp, slack
|
# Supported platform keys: cli, telegram, discord, whatsapp, slack, qqbot
|
||||||
#
|
#
|
||||||
# Examples:
|
# Examples:
|
||||||
#
|
#
|
||||||
|
|
@ -552,7 +552,7 @@ agent:
|
||||||
# slack: hermes-slack (same as telegram)
|
# slack: hermes-slack (same as telegram)
|
||||||
# signal: hermes-signal (same as telegram)
|
# signal: hermes-signal (same as telegram)
|
||||||
# homeassistant: hermes-homeassistant (same as telegram)
|
# homeassistant: hermes-homeassistant (same as telegram)
|
||||||
# qq: hermes-qq (same as telegram)
|
# qqbot: hermes-qqbot (same as telegram)
|
||||||
#
|
#
|
||||||
platform_toolsets:
|
platform_toolsets:
|
||||||
cli: [hermes-cli]
|
cli: [hermes-cli]
|
||||||
|
|
@ -562,7 +562,7 @@ platform_toolsets:
|
||||||
slack: [hermes-slack]
|
slack: [hermes-slack]
|
||||||
signal: [hermes-signal]
|
signal: [hermes-signal]
|
||||||
homeassistant: [hermes-homeassistant]
|
homeassistant: [hermes-homeassistant]
|
||||||
qq: [hermes-qq]
|
qqbot: [hermes-qqbot]
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Available toolsets (use these names in platform_toolsets or the toolsets list)
|
# Available toolsets (use these names in platform_toolsets or the toolsets list)
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@ logger = logging.getLogger(__name__)
|
||||||
_KNOWN_DELIVERY_PLATFORMS = frozenset({
|
_KNOWN_DELIVERY_PLATFORMS = frozenset({
|
||||||
"telegram", "discord", "slack", "whatsapp", "signal",
|
"telegram", "discord", "slack", "whatsapp", "signal",
|
||||||
"matrix", "mattermost", "homeassistant", "dingtalk", "feishu",
|
"matrix", "mattermost", "homeassistant", "dingtalk", "feishu",
|
||||||
"wecom", "wecom_callback", "weixin", "sms", "email", "webhook", "bluebubbles", "qq",
|
"wecom", "wecom_callback", "weixin", "sms", "email", "webhook", "bluebubbles",
|
||||||
|
"qqbot",
|
||||||
})
|
})
|
||||||
|
|
||||||
from cron.jobs import get_due_jobs, mark_job_run, save_job_output, advance_next_run
|
from cron.jobs import get_due_jobs, mark_job_run, save_job_output, advance_next_run
|
||||||
|
|
@ -254,7 +255,7 @@ def _deliver_result(job: dict, content: str, adapters=None, loop=None) -> Option
|
||||||
"email": Platform.EMAIL,
|
"email": Platform.EMAIL,
|
||||||
"sms": Platform.SMS,
|
"sms": Platform.SMS,
|
||||||
"bluebubbles": Platform.BLUEBUBBLES,
|
"bluebubbles": Platform.BLUEBUBBLES,
|
||||||
"qq": Platform.QQ,
|
"qqbot": Platform.QQBOT,
|
||||||
}
|
}
|
||||||
platform = platform_map.get(platform_name.lower())
|
platform = platform_map.get(platform_name.lower())
|
||||||
if not platform:
|
if not platform:
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ class Platform(Enum):
|
||||||
WECOM_CALLBACK = "wecom_callback"
|
WECOM_CALLBACK = "wecom_callback"
|
||||||
WEIXIN = "weixin"
|
WEIXIN = "weixin"
|
||||||
BLUEBUBBLES = "bluebubbles"
|
BLUEBUBBLES = "bluebubbles"
|
||||||
QQ = "qq"
|
QQBOT = "qqbot"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -304,8 +304,8 @@ class GatewayConfig:
|
||||||
# BlueBubbles uses extra dict for local server config
|
# BlueBubbles uses extra dict for local server config
|
||||||
elif platform == Platform.BLUEBUBBLES and config.extra.get("server_url") and config.extra.get("password"):
|
elif platform == Platform.BLUEBUBBLES and config.extra.get("server_url") and config.extra.get("password"):
|
||||||
connected.append(platform)
|
connected.append(platform)
|
||||||
# QQ uses extra dict for app credentials
|
# QQBot uses extra dict for app credentials
|
||||||
elif platform == Platform.QQ and config.extra.get("app_id") and config.extra.get("client_secret"):
|
elif platform == Platform.QQBOT and config.extra.get("app_id") and config.extra.get("client_secret"):
|
||||||
connected.append(platform)
|
connected.append(platform)
|
||||||
return connected
|
return connected
|
||||||
|
|
||||||
|
|
@ -1117,10 +1117,10 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
qq_app_id = os.getenv("QQ_APP_ID")
|
qq_app_id = os.getenv("QQ_APP_ID")
|
||||||
qq_client_secret = os.getenv("QQ_CLIENT_SECRET")
|
qq_client_secret = os.getenv("QQ_CLIENT_SECRET")
|
||||||
if qq_app_id or qq_client_secret:
|
if qq_app_id or qq_client_secret:
|
||||||
if Platform.QQ not in config.platforms:
|
if Platform.QQBOT not in config.platforms:
|
||||||
config.platforms[Platform.QQ] = PlatformConfig()
|
config.platforms[Platform.QQBOT] = PlatformConfig()
|
||||||
config.platforms[Platform.QQ].enabled = True
|
config.platforms[Platform.QQBOT].enabled = True
|
||||||
extra = config.platforms[Platform.QQ].extra
|
extra = config.platforms[Platform.QQBOT].extra
|
||||||
if qq_app_id:
|
if qq_app_id:
|
||||||
extra["app_id"] = qq_app_id
|
extra["app_id"] = qq_app_id
|
||||||
if qq_client_secret:
|
if qq_client_secret:
|
||||||
|
|
@ -1133,8 +1133,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
extra["group_allow_from"] = qq_group_allowed
|
extra["group_allow_from"] = qq_group_allowed
|
||||||
qq_home = os.getenv("QQ_HOME_CHANNEL", "").strip()
|
qq_home = os.getenv("QQ_HOME_CHANNEL", "").strip()
|
||||||
if qq_home:
|
if qq_home:
|
||||||
config.platforms[Platform.QQ].home_channel = HomeChannel(
|
config.platforms[Platform.QQBOT].home_channel = HomeChannel(
|
||||||
platform=Platform.QQ,
|
platform=Platform.QQBOT,
|
||||||
chat_id=qq_home,
|
chat_id=qq_home,
|
||||||
name=os.getenv("QQ_HOME_CHANNEL_NAME", "Home"),
|
name=os.getenv("QQ_HOME_CHANNEL_NAME", "Home"),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ Each adapter handles:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .base import BasePlatformAdapter, MessageEvent, SendResult
|
from .base import BasePlatformAdapter, MessageEvent, SendResult
|
||||||
from .qq import QQAdapter
|
from .qqbot import QQAdapter
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BasePlatformAdapter",
|
"BasePlatformAdapter",
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
MAX_MESSAGE_LENGTH = MAX_MESSAGE_LENGTH
|
MAX_MESSAGE_LENGTH = MAX_MESSAGE_LENGTH
|
||||||
|
|
||||||
def __init__(self, config: PlatformConfig):
|
def __init__(self, config: PlatformConfig):
|
||||||
super().__init__(config, Platform.QQ)
|
super().__init__(config, Platform.QQBOT)
|
||||||
|
|
||||||
extra = config.extra or {}
|
extra = config.extra or {}
|
||||||
self._app_id = str(extra.get("app_id") or os.getenv("QQ_APP_ID", "")).strip()
|
self._app_id = str(extra.get("app_id") or os.getenv("QQ_APP_ID", "")).strip()
|
||||||
|
|
@ -194,7 +194,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return "QQ"
|
return "QQBot"
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Connection lifecycle
|
# Connection lifecycle
|
||||||
|
|
@ -658,7 +658,7 @@ class QQAdapter(BasePlatformAdapter):
|
||||||
try:
|
try:
|
||||||
payload = json.loads(raw)
|
payload = json.loads(raw)
|
||||||
except Exception:
|
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 None
|
||||||
return payload if isinstance(payload, dict) else None
|
return payload if isinstance(payload, dict) else None
|
||||||
|
|
||||||
|
|
@ -2257,8 +2257,11 @@ class GatewayRunner:
|
||||||
return None
|
return None
|
||||||
return BlueBubblesAdapter(config)
|
return BlueBubblesAdapter(config)
|
||||||
|
|
||||||
elif platform == Platform.QQ:
|
elif platform == Platform.QQBOT:
|
||||||
from gateway.platforms.qq import QQAdapter
|
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 QQAdapter(config)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
@ -2302,7 +2305,7 @@ class GatewayRunner:
|
||||||
Platform.WECOM_CALLBACK: "WECOM_CALLBACK_ALLOWED_USERS",
|
Platform.WECOM_CALLBACK: "WECOM_CALLBACK_ALLOWED_USERS",
|
||||||
Platform.WEIXIN: "WEIXIN_ALLOWED_USERS",
|
Platform.WEIXIN: "WEIXIN_ALLOWED_USERS",
|
||||||
Platform.BLUEBUBBLES: "BLUEBUBBLES_ALLOWED_USERS",
|
Platform.BLUEBUBBLES: "BLUEBUBBLES_ALLOWED_USERS",
|
||||||
Platform.QQ: "QQ_ALLOWED_USERS",
|
Platform.QQBOT: "QQ_ALLOWED_USERS",
|
||||||
}
|
}
|
||||||
platform_allow_all_map = {
|
platform_allow_all_map = {
|
||||||
Platform.TELEGRAM: "TELEGRAM_ALLOW_ALL_USERS",
|
Platform.TELEGRAM: "TELEGRAM_ALLOW_ALL_USERS",
|
||||||
|
|
@ -2320,7 +2323,7 @@ class GatewayRunner:
|
||||||
Platform.WECOM_CALLBACK: "WECOM_CALLBACK_ALLOW_ALL_USERS",
|
Platform.WECOM_CALLBACK: "WECOM_CALLBACK_ALLOW_ALL_USERS",
|
||||||
Platform.WEIXIN: "WEIXIN_ALLOW_ALL_USERS",
|
Platform.WEIXIN: "WEIXIN_ALLOW_ALL_USERS",
|
||||||
Platform.BLUEBUBBLES: "BLUEBUBBLES_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)
|
# Per-platform allow-all flag (e.g., DISCORD_ALLOW_ALL_USERS=true)
|
||||||
|
|
@ -7817,13 +7820,14 @@ class GatewayRunner:
|
||||||
_adapter = self.adapters.get(source.platform)
|
_adapter = self.adapters.get(source.platform)
|
||||||
if _adapter:
|
if _adapter:
|
||||||
# Platforms that don't support editing sent messages
|
# Platforms that don't support editing sent messages
|
||||||
# (e.g. WeChat) must not show a cursor in intermediate
|
# (e.g. QQ, WeChat) should skip streaming entirely —
|
||||||
# sends — the cursor would be permanently visible because
|
# without edit support, the consumer sends a partial
|
||||||
# it can never be edited away. Use an empty cursor for
|
# first message that can never be updated, resulting in
|
||||||
# such platforms so streaming still delivers the final
|
# duplicate messages (partial + final).
|
||||||
# response, just without the typing indicator.
|
|
||||||
_adapter_supports_edit = getattr(_adapter, "SUPPORTS_MESSAGE_EDITING", True)
|
_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
|
# Some Matrix clients render the streaming cursor
|
||||||
# as a visible tofu/white-box artifact. Keep
|
# as a visible tofu/white-box artifact. Keep
|
||||||
# streaming text on Matrix, but suppress the cursor.
|
# streaming text on Matrix, but suppress the cursor.
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,9 @@ _EXTRA_ENV_KEYS = frozenset({
|
||||||
"WEIXIN_HOME_CHANNEL", "WEIXIN_HOME_CHANNEL_NAME", "WEIXIN_DM_POLICY", "WEIXIN_GROUP_POLICY",
|
"WEIXIN_HOME_CHANNEL", "WEIXIN_HOME_CHANNEL_NAME", "WEIXIN_DM_POLICY", "WEIXIN_GROUP_POLICY",
|
||||||
"WEIXIN_ALLOWED_USERS", "WEIXIN_GROUP_ALLOWED_USERS", "WEIXIN_ALLOW_ALL_USERS",
|
"WEIXIN_ALLOWED_USERS", "WEIXIN_GROUP_ALLOWED_USERS", "WEIXIN_ALLOW_ALL_USERS",
|
||||||
"BLUEBUBBLES_SERVER_URL", "BLUEBUBBLES_PASSWORD",
|
"BLUEBUBBLES_SERVER_URL", "BLUEBUBBLES_PASSWORD",
|
||||||
"QQ_APP_ID", "QQ_CLIENT_SECRET",
|
"QQ_APP_ID", "QQ_CLIENT_SECRET", "QQ_HOME_CHANNEL", "QQ_HOME_CHANNEL_NAME",
|
||||||
"QQ_ALLOWED_USERS", "QQ_GROUP_ALLOWED_USERS", "QQ_ALLOW_ALL_USERS",
|
"QQ_ALLOWED_USERS", "QQ_GROUP_ALLOWED_USERS", "QQ_ALLOW_ALL_USERS", "QQ_MARKDOWN_SUPPORT",
|
||||||
"QQ_HOME_CHANNEL", "QQ_HOME_CHANNEL_NAME", "QQ_SANDBOX",
|
"QQ_STT_API_KEY", "QQ_STT_BASE_URL", "QQ_STT_MODEL",
|
||||||
"TERMINAL_ENV", "TERMINAL_SSH_KEY", "TERMINAL_SSH_PORT",
|
"TERMINAL_ENV", "TERMINAL_SSH_KEY", "TERMINAL_SSH_PORT",
|
||||||
"WHATSAPP_MODE", "WHATSAPP_ENABLED",
|
"WHATSAPP_MODE", "WHATSAPP_ENABLED",
|
||||||
"MATTERMOST_HOME_CHANNEL", "MATTERMOST_REPLY_MODE",
|
"MATTERMOST_HOME_CHANNEL", "MATTERMOST_REPLY_MODE",
|
||||||
|
|
|
||||||
|
|
@ -1914,27 +1914,26 @@ _PLATFORMS = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "qq",
|
"key": "qqbot",
|
||||||
"label": "QQ Bot",
|
"label": "QQ Bot",
|
||||||
"emoji": "💬",
|
"emoji": "🐧",
|
||||||
"token_var": "QQ_APP_ID",
|
"token_var": "QQ_APP_ID",
|
||||||
"setup_instructions": [
|
"setup_instructions": [
|
||||||
"1. Go to https://open.qq.com/ and create an application",
|
"1. Register a QQ Bot application at q.qq.com",
|
||||||
"2. In the application dashboard, create a QQ Bot",
|
"2. Note your App ID and App Secret from the application page",
|
||||||
"3. Note your App ID and App Secret",
|
"3. Enable the required intents (C2C, Group, Guild messages)",
|
||||||
"4. Configure the WebSocket Gateway URL in QQ Open Platform settings",
|
"4. Configure sandbox or publish the bot",
|
||||||
"5. Set up message push URL if needed for event callbacks",
|
|
||||||
],
|
],
|
||||||
"vars": [
|
"vars": [
|
||||||
{"name": "QQ_APP_ID", "prompt": "App ID", "password": False,
|
{"name": "QQ_APP_ID", "prompt": "QQ Bot App ID", "password": False,
|
||||||
"help": "Paste the App ID from QQ Open Platform."},
|
"help": "Your QQ Bot App ID from q.qq.com."},
|
||||||
{"name": "QQ_CLIENT_SECRET", "prompt": "App Secret", "password": True,
|
{"name": "QQ_CLIENT_SECRET", "prompt": "QQ Bot App Secret", "password": True,
|
||||||
"help": "Paste the App Secret from QQ Open Platform."},
|
"help": "Your QQ Bot App Secret from q.qq.com."},
|
||||||
{"name": "QQ_ALLOWED_USERS", "prompt": "Allowed QQ user IDs (comma-separated, or empty for DM pairing)", "password": False,
|
{"name": "QQ_ALLOWED_USERS", "prompt": "Allowed user OpenIDs (comma-separated, leave empty for open access)", "password": False,
|
||||||
"is_allowlist": True,
|
"is_allowlist": True,
|
||||||
"help": "Optional — pre-authorize specific users. Leave empty to use DM pairing instead."},
|
"help": "Optional — restrict DM access to specific user OpenIDs."},
|
||||||
{"name": "QQ_HOME_CHANNEL", "prompt": "Home channel (QQ group ID for cron/notifications, or empty)", "password": False,
|
{"name": "QQ_HOME_CHANNEL", "prompt": "Home channel (user/group OpenID for cron delivery, or empty)", "password": False,
|
||||||
"help": "QQ group ID to deliver cron results and notifications to."},
|
"help": "OpenID to deliver cron results and notifications to."},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ PLATFORMS: OrderedDict[str, PlatformInfo] = OrderedDict([
|
||||||
("wecom", PlatformInfo(label="💬 WeCom", default_toolset="hermes-wecom")),
|
("wecom", PlatformInfo(label="💬 WeCom", default_toolset="hermes-wecom")),
|
||||||
("wecom_callback", PlatformInfo(label="💬 WeCom Callback", default_toolset="hermes-wecom-callback")),
|
("wecom_callback", PlatformInfo(label="💬 WeCom Callback", default_toolset="hermes-wecom-callback")),
|
||||||
("weixin", PlatformInfo(label="💬 Weixin", default_toolset="hermes-weixin")),
|
("weixin", PlatformInfo(label="💬 Weixin", default_toolset="hermes-weixin")),
|
||||||
("qq", PlatformInfo(label="💬 QQ", default_toolset="hermes-qq")),
|
("qqbot", PlatformInfo(label="💬 QQBot", default_toolset="hermes-qqbot")),
|
||||||
("webhook", PlatformInfo(label="🔗 Webhook", default_toolset="hermes-webhook")),
|
("webhook", PlatformInfo(label="🔗 Webhook", default_toolset="hermes-webhook")),
|
||||||
("api_server", PlatformInfo(label="🌐 API Server", default_toolset="hermes-api-server")),
|
("api_server", PlatformInfo(label="🌐 API Server", default_toolset="hermes-api-server")),
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -1969,6 +1969,54 @@ def _setup_wecom_callback():
|
||||||
_gw_setup()
|
_gw_setup()
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_qqbot():
|
||||||
|
"""Configure QQ Bot gateway."""
|
||||||
|
print_header("QQ Bot")
|
||||||
|
existing = get_env_value("QQ_APP_ID")
|
||||||
|
if existing:
|
||||||
|
print_info("QQ Bot: already configured")
|
||||||
|
if not prompt_yes_no("Reconfigure QQ Bot?", False):
|
||||||
|
return
|
||||||
|
|
||||||
|
print_info("Connects Hermes to QQ via the Official QQ Bot API (v2).")
|
||||||
|
print_info(" Requires a QQ Bot application at q.qq.com")
|
||||||
|
print_info(" Reference: https://bot.q.qq.com/wiki/develop/api-v2/")
|
||||||
|
print()
|
||||||
|
|
||||||
|
app_id = prompt("QQ Bot App ID")
|
||||||
|
if not app_id:
|
||||||
|
print_warning("App ID is required — skipping QQ Bot setup")
|
||||||
|
return
|
||||||
|
save_env_value("QQ_APP_ID", app_id.strip())
|
||||||
|
|
||||||
|
client_secret = prompt("QQ Bot App Secret", password=True)
|
||||||
|
if not client_secret:
|
||||||
|
print_warning("App Secret is required — skipping QQ Bot setup")
|
||||||
|
return
|
||||||
|
save_env_value("QQ_CLIENT_SECRET", client_secret)
|
||||||
|
print_success("QQ Bot credentials saved")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print_info("🔒 Security: Restrict who can DM your bot")
|
||||||
|
print_info(" Use QQ user OpenIDs (found in event payloads)")
|
||||||
|
print()
|
||||||
|
allowed_users = prompt("Allowed user OpenIDs (comma-separated, leave empty for open access)")
|
||||||
|
if allowed_users:
|
||||||
|
save_env_value("QQ_ALLOWED_USERS", allowed_users.replace(" ", ""))
|
||||||
|
print_success("QQ Bot allowlist configured")
|
||||||
|
else:
|
||||||
|
print_info("⚠️ No allowlist set — anyone can DM the bot!")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print_info("📬 Home Channel: OpenID for cron job delivery and notifications.")
|
||||||
|
home_channel = prompt("Home channel OpenID (leave empty to set later)")
|
||||||
|
if home_channel:
|
||||||
|
save_env_value("QQ_HOME_CHANNEL", home_channel)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print_success("QQ Bot configured!")
|
||||||
|
|
||||||
|
|
||||||
def _setup_bluebubbles():
|
def _setup_bluebubbles():
|
||||||
"""Configure BlueBubbles iMessage gateway."""
|
"""Configure BlueBubbles iMessage gateway."""
|
||||||
print_header("BlueBubbles (iMessage)")
|
print_header("BlueBubbles (iMessage)")
|
||||||
|
|
@ -2034,10 +2082,10 @@ def _setup_bluebubbles():
|
||||||
print_info(" Install: https://docs.bluebubbles.app/helper-bundle/installation")
|
print_info(" Install: https://docs.bluebubbles.app/helper-bundle/installation")
|
||||||
|
|
||||||
|
|
||||||
def _setup_qq():
|
def _setup_qqbot():
|
||||||
"""Configure QQ Bot (Official API v2) via standard platform setup."""
|
"""Configure QQ Bot (Official API v2) via standard platform setup."""
|
||||||
from hermes_cli.gateway import _PLATFORMS
|
from hermes_cli.gateway import _PLATFORMS
|
||||||
qq_platform = next((p for p in _PLATFORMS if p["key"] == "qq"), None)
|
qq_platform = next((p for p in _PLATFORMS if p["key"] == "qqbot"), None)
|
||||||
if qq_platform:
|
if qq_platform:
|
||||||
from hermes_cli.gateway import _setup_standard_platform
|
from hermes_cli.gateway import _setup_standard_platform
|
||||||
_setup_standard_platform(qq_platform)
|
_setup_standard_platform(qq_platform)
|
||||||
|
|
@ -2106,7 +2154,7 @@ _GATEWAY_PLATFORMS = [
|
||||||
("WeCom Callback (Self-Built App)", "WECOM_CALLBACK_CORP_ID", _setup_wecom_callback),
|
("WeCom Callback (Self-Built App)", "WECOM_CALLBACK_CORP_ID", _setup_wecom_callback),
|
||||||
("Weixin (WeChat)", "WEIXIN_ACCOUNT_ID", _setup_weixin),
|
("Weixin (WeChat)", "WEIXIN_ACCOUNT_ID", _setup_weixin),
|
||||||
("BlueBubbles (iMessage)", "BLUEBUBBLES_SERVER_URL", _setup_bluebubbles),
|
("BlueBubbles (iMessage)", "BLUEBUBBLES_SERVER_URL", _setup_bluebubbles),
|
||||||
("QQ Bot", "QQ_APP_ID", _setup_qq),
|
("QQ Bot", "QQ_APP_ID", _setup_qqbot),
|
||||||
("Webhooks (GitHub, GitLab, etc.)", "WEBHOOK_ENABLED", _setup_webhooks),
|
("Webhooks (GitHub, GitLab, etc.)", "WEBHOOK_ENABLED", _setup_webhooks),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2158,6 +2206,7 @@ def setup_gateway(config: dict):
|
||||||
or get_env_value("WECOM_BOT_ID")
|
or get_env_value("WECOM_BOT_ID")
|
||||||
or get_env_value("WEIXIN_ACCOUNT_ID")
|
or get_env_value("WEIXIN_ACCOUNT_ID")
|
||||||
or get_env_value("BLUEBUBBLES_SERVER_URL")
|
or get_env_value("BLUEBUBBLES_SERVER_URL")
|
||||||
|
or get_env_value("QQ_APP_ID")
|
||||||
or get_env_value("WEBHOOK_ENABLED")
|
or get_env_value("WEBHOOK_ENABLED")
|
||||||
)
|
)
|
||||||
if any_messaging:
|
if any_messaging:
|
||||||
|
|
@ -2179,6 +2228,8 @@ def setup_gateway(config: dict):
|
||||||
missing_home.append("Slack")
|
missing_home.append("Slack")
|
||||||
if get_env_value("BLUEBUBBLES_SERVER_URL") and not get_env_value("BLUEBUBBLES_HOME_CHANNEL"):
|
if get_env_value("BLUEBUBBLES_SERVER_URL") and not get_env_value("BLUEBUBBLES_HOME_CHANNEL"):
|
||||||
missing_home.append("BlueBubbles")
|
missing_home.append("BlueBubbles")
|
||||||
|
if get_env_value("QQ_APP_ID") and not get_env_value("QQ_HOME_CHANNEL"):
|
||||||
|
missing_home.append("QQBot")
|
||||||
|
|
||||||
if missing_home:
|
if missing_home:
|
||||||
print()
|
print()
|
||||||
|
|
|
||||||
|
|
@ -305,7 +305,7 @@ def show_status(args):
|
||||||
"WeCom Callback": ("WECOM_CALLBACK_CORP_ID", None),
|
"WeCom Callback": ("WECOM_CALLBACK_CORP_ID", None),
|
||||||
"Weixin": ("WEIXIN_ACCOUNT_ID", "WEIXIN_HOME_CHANNEL"),
|
"Weixin": ("WEIXIN_ACCOUNT_ID", "WEIXIN_HOME_CHANNEL"),
|
||||||
"BlueBubbles": ("BLUEBUBBLES_SERVER_URL", "BLUEBUBBLES_HOME_CHANNEL"),
|
"BlueBubbles": ("BLUEBUBBLES_SERVER_URL", "BLUEBUBBLES_HOME_CHANNEL"),
|
||||||
"QQ": ("QQ_APP_ID", "QQ_HOME_CHANNEL"),
|
"QQBot": ("QQ_APP_ID", "QQ_HOME_CHANNEL"),
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, (token_var, home_var) in platforms.items():
|
for name, (token_var, home_var) in platforms.items():
|
||||||
|
|
|
||||||
|
|
@ -427,7 +427,7 @@ def _get_enabled_platforms() -> List[str]:
|
||||||
if get_env_value("WHATSAPP_ENABLED"):
|
if get_env_value("WHATSAPP_ENABLED"):
|
||||||
enabled.append("whatsapp")
|
enabled.append("whatsapp")
|
||||||
if get_env_value("QQ_APP_ID"):
|
if get_env_value("QQ_APP_ID"):
|
||||||
enabled.append("qq")
|
enabled.append("qqbot")
|
||||||
return enabled
|
return enabled
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ def _make_config(**extra):
|
||||||
|
|
||||||
class TestQQRequirements:
|
class TestQQRequirements:
|
||||||
def test_returns_bool(self):
|
def test_returns_bool(self):
|
||||||
from gateway.platforms.qq import check_qq_requirements
|
from gateway.platforms.qqbot import check_qq_requirements
|
||||||
result = check_qq_requirements()
|
result = check_qq_requirements()
|
||||||
assert isinstance(result, bool)
|
assert isinstance(result, bool)
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ class TestQQRequirements:
|
||||||
|
|
||||||
class TestQQAdapterInit:
|
class TestQQAdapterInit:
|
||||||
def _make(self, **extra):
|
def _make(self, **extra):
|
||||||
from gateway.platforms.qq import QQAdapter
|
from gateway.platforms.qqbot import QQAdapter
|
||||||
return QQAdapter(_make_config(**extra))
|
return QQAdapter(_make_config(**extra))
|
||||||
|
|
||||||
def test_basic_attributes(self):
|
def test_basic_attributes(self):
|
||||||
|
|
@ -93,7 +93,7 @@ class TestQQAdapterInit:
|
||||||
|
|
||||||
def test_name_property(self):
|
def test_name_property(self):
|
||||||
adapter = self._make(app_id="a", client_secret="b")
|
adapter = self._make(app_id="a", client_secret="b")
|
||||||
assert adapter.name == "QQ"
|
assert adapter.name == "QQBOT"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -102,7 +102,7 @@ class TestQQAdapterInit:
|
||||||
|
|
||||||
class TestCoerceList:
|
class TestCoerceList:
|
||||||
def _fn(self, value):
|
def _fn(self, value):
|
||||||
from gateway.platforms.qq import _coerce_list
|
from gateway.platforms.qqbot import _coerce_list
|
||||||
return _coerce_list(value)
|
return _coerce_list(value)
|
||||||
|
|
||||||
def test_none(self):
|
def test_none(self):
|
||||||
|
|
@ -130,7 +130,7 @@ class TestCoerceList:
|
||||||
|
|
||||||
class TestIsVoiceContentType:
|
class TestIsVoiceContentType:
|
||||||
def _fn(self, content_type, filename):
|
def _fn(self, content_type, filename):
|
||||||
from gateway.platforms.qq import QQAdapter
|
from gateway.platforms.qqbot import QQAdapter
|
||||||
return QQAdapter._is_voice_content_type(content_type, filename)
|
return QQAdapter._is_voice_content_type(content_type, filename)
|
||||||
|
|
||||||
def test_voice_content_type(self):
|
def test_voice_content_type(self):
|
||||||
|
|
@ -155,7 +155,7 @@ class TestIsVoiceContentType:
|
||||||
|
|
||||||
class TestStripAtMention:
|
class TestStripAtMention:
|
||||||
def _fn(self, content):
|
def _fn(self, content):
|
||||||
from gateway.platforms.qq import QQAdapter
|
from gateway.platforms.qqbot import QQAdapter
|
||||||
return QQAdapter._strip_at_mention(content)
|
return QQAdapter._strip_at_mention(content)
|
||||||
|
|
||||||
def test_removes_mention(self):
|
def test_removes_mention(self):
|
||||||
|
|
@ -179,7 +179,7 @@ class TestStripAtMention:
|
||||||
|
|
||||||
class TestDmAllowed:
|
class TestDmAllowed:
|
||||||
def _make_adapter(self, **extra):
|
def _make_adapter(self, **extra):
|
||||||
from gateway.platforms.qq import QQAdapter
|
from gateway.platforms.qqbot import QQAdapter
|
||||||
return QQAdapter(_make_config(**extra))
|
return QQAdapter(_make_config(**extra))
|
||||||
|
|
||||||
def test_open_policy(self):
|
def test_open_policy(self):
|
||||||
|
|
@ -209,7 +209,7 @@ class TestDmAllowed:
|
||||||
|
|
||||||
class TestGroupAllowed:
|
class TestGroupAllowed:
|
||||||
def _make_adapter(self, **extra):
|
def _make_adapter(self, **extra):
|
||||||
from gateway.platforms.qq import QQAdapter
|
from gateway.platforms.qqbot import QQAdapter
|
||||||
return QQAdapter(_make_config(**extra))
|
return QQAdapter(_make_config(**extra))
|
||||||
|
|
||||||
def test_open_policy(self):
|
def test_open_policy(self):
|
||||||
|
|
@ -231,7 +231,7 @@ class TestGroupAllowed:
|
||||||
|
|
||||||
class TestResolveSTTConfig:
|
class TestResolveSTTConfig:
|
||||||
def _make_adapter(self, **extra):
|
def _make_adapter(self, **extra):
|
||||||
from gateway.platforms.qq import QQAdapter
|
from gateway.platforms.qqbot import QQAdapter
|
||||||
return QQAdapter(_make_config(**extra))
|
return QQAdapter(_make_config(**extra))
|
||||||
|
|
||||||
def test_no_config(self):
|
def test_no_config(self):
|
||||||
|
|
@ -273,7 +273,7 @@ class TestResolveSTTConfig:
|
||||||
|
|
||||||
class TestDetectMessageType:
|
class TestDetectMessageType:
|
||||||
def _fn(self, media_urls, media_types):
|
def _fn(self, media_urls, media_types):
|
||||||
from gateway.platforms.qq import QQAdapter
|
from gateway.platforms.qqbot import QQAdapter
|
||||||
return QQAdapter._detect_message_type(media_urls, media_types)
|
return QQAdapter._detect_message_type(media_urls, media_types)
|
||||||
|
|
||||||
def test_no_media(self):
|
def test_no_media(self):
|
||||||
|
|
@ -299,24 +299,24 @@ class TestDetectMessageType:
|
||||||
|
|
||||||
class TestQQCloseError:
|
class TestQQCloseError:
|
||||||
def test_attributes(self):
|
def test_attributes(self):
|
||||||
from gateway.platforms.qq import QQCloseError
|
from gateway.platforms.qqbot import QQCloseError
|
||||||
err = QQCloseError(4004, "bad token")
|
err = QQCloseError(4004, "bad token")
|
||||||
assert err.code == 4004
|
assert err.code == 4004
|
||||||
assert err.reason == "bad token"
|
assert err.reason == "bad token"
|
||||||
|
|
||||||
def test_code_none(self):
|
def test_code_none(self):
|
||||||
from gateway.platforms.qq import QQCloseError
|
from gateway.platforms.qqbot import QQCloseError
|
||||||
err = QQCloseError(None, "")
|
err = QQCloseError(None, "")
|
||||||
assert err.code is None
|
assert err.code is None
|
||||||
|
|
||||||
def test_string_to_int(self):
|
def test_string_to_int(self):
|
||||||
from gateway.platforms.qq import QQCloseError
|
from gateway.platforms.qqbot import QQCloseError
|
||||||
err = QQCloseError("4914", "banned")
|
err = QQCloseError("4914", "banned")
|
||||||
assert err.code == 4914
|
assert err.code == 4914
|
||||||
assert err.reason == "banned"
|
assert err.reason == "banned"
|
||||||
|
|
||||||
def test_message_format(self):
|
def test_message_format(self):
|
||||||
from gateway.platforms.qq import QQCloseError
|
from gateway.platforms.qqbot import QQCloseError
|
||||||
err = QQCloseError(4008, "rate limit")
|
err = QQCloseError(4008, "rate limit")
|
||||||
assert "4008" in str(err)
|
assert "4008" in str(err)
|
||||||
assert "rate limit" in str(err)
|
assert "rate limit" in str(err)
|
||||||
|
|
@ -328,7 +328,7 @@ class TestQQCloseError:
|
||||||
|
|
||||||
class TestDispatchPayload:
|
class TestDispatchPayload:
|
||||||
def _make_adapter(self, **extra):
|
def _make_adapter(self, **extra):
|
||||||
from gateway.platforms.qq import QQAdapter
|
from gateway.platforms.qqbot import QQAdapter
|
||||||
adapter = QQAdapter(_make_config(**extra))
|
adapter = QQAdapter(_make_config(**extra))
|
||||||
return adapter
|
return adapter
|
||||||
|
|
||||||
|
|
@ -368,7 +368,7 @@ class TestDispatchPayload:
|
||||||
|
|
||||||
class TestReadyHandling:
|
class TestReadyHandling:
|
||||||
def _make_adapter(self, **extra):
|
def _make_adapter(self, **extra):
|
||||||
from gateway.platforms.qq import QQAdapter
|
from gateway.platforms.qqbot import QQAdapter
|
||||||
return QQAdapter(_make_config(**extra))
|
return QQAdapter(_make_config(**extra))
|
||||||
|
|
||||||
def test_ready_stores_session(self):
|
def test_ready_stores_session(self):
|
||||||
|
|
@ -398,7 +398,7 @@ class TestReadyHandling:
|
||||||
|
|
||||||
class TestParseJson:
|
class TestParseJson:
|
||||||
def _fn(self, raw):
|
def _fn(self, raw):
|
||||||
from gateway.platforms.qq import QQAdapter
|
from gateway.platforms.qqbot import QQAdapter
|
||||||
return QQAdapter._parse_json(raw)
|
return QQAdapter._parse_json(raw)
|
||||||
|
|
||||||
def test_valid_json(self):
|
def test_valid_json(self):
|
||||||
|
|
@ -428,7 +428,7 @@ class TestParseJson:
|
||||||
|
|
||||||
class TestBuildTextBody:
|
class TestBuildTextBody:
|
||||||
def _make_adapter(self, **extra):
|
def _make_adapter(self, **extra):
|
||||||
from gateway.platforms.qq import QQAdapter
|
from gateway.platforms.qqbot import QQAdapter
|
||||||
return QQAdapter(_make_config(**extra))
|
return QQAdapter(_make_config(**extra))
|
||||||
|
|
||||||
def test_plain_text(self):
|
def test_plain_text(self):
|
||||||
|
|
@ -152,6 +152,7 @@ def _handle_send(args):
|
||||||
"whatsapp": Platform.WHATSAPP,
|
"whatsapp": Platform.WHATSAPP,
|
||||||
"signal": Platform.SIGNAL,
|
"signal": Platform.SIGNAL,
|
||||||
"bluebubbles": Platform.BLUEBUBBLES,
|
"bluebubbles": Platform.BLUEBUBBLES,
|
||||||
|
"qqbot": Platform.QQBOT,
|
||||||
"matrix": Platform.MATRIX,
|
"matrix": Platform.MATRIX,
|
||||||
"mattermost": Platform.MATTERMOST,
|
"mattermost": Platform.MATTERMOST,
|
||||||
"homeassistant": Platform.HOMEASSISTANT,
|
"homeassistant": Platform.HOMEASSISTANT,
|
||||||
|
|
@ -160,7 +161,6 @@ def _handle_send(args):
|
||||||
"wecom": Platform.WECOM,
|
"wecom": Platform.WECOM,
|
||||||
"wecom_callback": Platform.WECOM_CALLBACK,
|
"wecom_callback": Platform.WECOM_CALLBACK,
|
||||||
"weixin": Platform.WEIXIN,
|
"weixin": Platform.WEIXIN,
|
||||||
"qq": Platform.QQ,
|
|
||||||
"email": Platform.EMAIL,
|
"email": Platform.EMAIL,
|
||||||
"sms": Platform.SMS,
|
"sms": Platform.SMS,
|
||||||
}
|
}
|
||||||
|
|
@ -427,8 +427,8 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None,
|
||||||
result = await _send_wecom(pconfig.extra, chat_id, chunk)
|
result = await _send_wecom(pconfig.extra, chat_id, chunk)
|
||||||
elif platform == Platform.BLUEBUBBLES:
|
elif platform == Platform.BLUEBUBBLES:
|
||||||
result = await _send_bluebubbles(pconfig.extra, chat_id, chunk)
|
result = await _send_bluebubbles(pconfig.extra, chat_id, chunk)
|
||||||
elif platform == Platform.QQ:
|
elif platform == Platform.QQBOT:
|
||||||
result = await _send_qq(pconfig.extra, chat_id, chunk)
|
result = await _send_qqbot(pconfig, chat_id, chunk)
|
||||||
else:
|
else:
|
||||||
result = {"error": f"Direct sending not yet implemented for {platform.value}"}
|
result = {"error": f"Direct sending not yet implemented for {platform.value}"}
|
||||||
|
|
||||||
|
|
@ -971,25 +971,6 @@ async def _send_bluebubbles(extra, chat_id, message):
|
||||||
return _error(f"BlueBubbles send failed: {e}")
|
return _error(f"BlueBubbles send failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def _send_qq(extra, chat_id, message):
|
|
||||||
"""Send via QQ Bot Official API v2 using the adapter's REST endpoint."""
|
|
||||||
try:
|
|
||||||
from gateway.platforms.qq import QQAdapter
|
|
||||||
except ImportError:
|
|
||||||
return {"error": "QQ adapter not available."}
|
|
||||||
|
|
||||||
try:
|
|
||||||
from gateway.config import PlatformConfig
|
|
||||||
pconfig = PlatformConfig(extra=extra)
|
|
||||||
adapter = QQAdapter(pconfig)
|
|
||||||
result = await adapter.send(chat_id, message)
|
|
||||||
if not result.success:
|
|
||||||
return _error(f"QQ send failed: {result.error}")
|
|
||||||
return {"success": True, "platform": "qq", "chat_id": chat_id, "message_id": result.message_id}
|
|
||||||
except Exception as e:
|
|
||||||
return _error(f"QQ send failed: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
async def _send_feishu(pconfig, chat_id, message, media_files=None, thread_id=None):
|
async def _send_feishu(pconfig, chat_id, message, media_files=None, thread_id=None):
|
||||||
"""Send via Feishu/Lark using the adapter's send pipeline."""
|
"""Send via Feishu/Lark using the adapter's send pipeline."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -1060,6 +1041,31 @@ def _check_send_message():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _send_qqbot(pconfig, chat_id, message):
|
||||||
|
"""Send via QQ Bot API using the adapter's REST API."""
|
||||||
|
try:
|
||||||
|
from gateway.platforms.qqbot import QQAdapter, check_qq_requirements
|
||||||
|
if not check_qq_requirements():
|
||||||
|
return {"error": "QQBot requirements not met (need aiohttp + httpx)."}
|
||||||
|
except ImportError:
|
||||||
|
return {"error": "QQBot adapter not available."}
|
||||||
|
|
||||||
|
try:
|
||||||
|
adapter = QQAdapter(pconfig)
|
||||||
|
connected = await adapter.connect()
|
||||||
|
if not connected:
|
||||||
|
return _error("QQBot: failed to connect to server")
|
||||||
|
try:
|
||||||
|
result = await adapter.send(chat_id, message)
|
||||||
|
if not result.success:
|
||||||
|
return _error(f"QQ send failed: {result.error}")
|
||||||
|
return {"success": True, "platform": "qqbot", "chat_id": chat_id, "message_id": result.message_id}
|
||||||
|
finally:
|
||||||
|
await adapter.disconnect()
|
||||||
|
except Exception as e:
|
||||||
|
return _error(f"QQ send failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
# --- Registry ---
|
# --- Registry ---
|
||||||
from tools.registry import registry, tool_error
|
from tools.registry import registry, tool_error
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -359,8 +359,8 @@ TOOLSETS = {
|
||||||
"includes": []
|
"includes": []
|
||||||
},
|
},
|
||||||
|
|
||||||
"hermes-qq": {
|
"hermes-qqbot": {
|
||||||
"description": "QQ Bot toolset - QQ messaging via Official Bot API v2 (full access)",
|
"description": "QQBot toolset - QQ messaging via Official Bot API v2 (full access)",
|
||||||
"tools": _HERMES_CORE_TOOLS,
|
"tools": _HERMES_CORE_TOOLS,
|
||||||
"includes": []
|
"includes": []
|
||||||
},
|
},
|
||||||
|
|
@ -392,7 +392,7 @@ TOOLSETS = {
|
||||||
"hermes-gateway": {
|
"hermes-gateway": {
|
||||||
"description": "Gateway toolset - union of all messaging platform tools",
|
"description": "Gateway toolset - union of all messaging platform tools",
|
||||||
"tools": [],
|
"tools": [],
|
||||||
"includes": ["hermes-telegram", "hermes-discord", "hermes-whatsapp", "hermes-slack", "hermes-signal", "hermes-bluebubbles", "hermes-homeassistant", "hermes-email", "hermes-sms", "hermes-mattermost", "hermes-matrix", "hermes-dingtalk", "hermes-feishu", "hermes-wecom", "hermes-wecom-callback", "hermes-weixin", "hermes-qq", "hermes-webhook"]
|
"includes": ["hermes-telegram", "hermes-discord", "hermes-whatsapp", "hermes-slack", "hermes-signal", "hermes-bluebubbles", "hermes-homeassistant", "hermes-email", "hermes-sms", "hermes-mattermost", "hermes-matrix", "hermes-dingtalk", "hermes-feishu", "hermes-wecom", "hermes-wecom-callback", "hermes-weixin", "hermes-qqbot", "hermes-webhook"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -262,20 +262,15 @@ For cloud sandbox backends, persistence is filesystem-oriented. `TERMINAL_LIFETI
|
||||||
| `BLUEBUBBLES_HOME_CHANNEL` | Phone/email for cron/notification delivery |
|
| `BLUEBUBBLES_HOME_CHANNEL` | Phone/email for cron/notification delivery |
|
||||||
| `BLUEBUBBLES_ALLOWED_USERS` | Comma-separated authorized users |
|
| `BLUEBUBBLES_ALLOWED_USERS` | Comma-separated authorized users |
|
||||||
| `BLUEBUBBLES_ALLOW_ALL_USERS` | Allow all users (`true`/`false`) |
|
| `BLUEBUBBLES_ALLOW_ALL_USERS` | Allow all users (`true`/`false`) |
|
||||||
|
| `QQ_APP_ID` | QQ Bot App ID from [q.qq.com](https://q.qq.com) |
|
||||||
#### QQ Bot
|
| `QQ_CLIENT_SECRET` | QQ Bot App Secret from [q.qq.com](https://q.qq.com) |
|
||||||
|
| `QQ_STT_API_KEY` | API key for external STT fallback provider (optional, used when QQ built-in ASR returns no text) |
|
||||||
| Variable | Description |
|
| `QQ_STT_BASE_URL` | Base URL for external STT provider (optional) |
|
||||||
|----------|-------------|
|
| `QQ_STT_MODEL` | Model name for external STT provider (optional) |
|
||||||
| `QQ_APP_ID` | QQ Bot App ID (from open.qq.com) |
|
| `QQ_ALLOWED_USERS` | Comma-separated QQ user openIDs allowed to message the bot |
|
||||||
| `QQ_CLIENT_SECRET` | QQ Bot App Secret |
|
| `QQ_GROUP_ALLOWED_USERS` | Comma-separated QQ group IDs for group @-message access |
|
||||||
| `QQ_SANDBOX` | Enable sandbox mode for testing (`true`/`false`) |
|
| `QQ_ALLOW_ALL_USERS` | Allow all users (`true`/`false`, overrides `QQ_ALLOWED_USERS`) |
|
||||||
| `QQ_ALLOWED_USERS` | Comma-separated QQ user IDs allowed to DM the bot |
|
| `QQ_HOME_CHANNEL` | QQ user/group openID for cron delivery and notifications |
|
||||||
| `QQ_GROUP_ALLOWED_USERS` | Comma-separated QQ user IDs allowed in group messages |
|
|
||||||
| `QQ_ALLOW_ALL_USERS` | Allow all QQ users (`true`/`false`) |
|
|
||||||
| `QQ_HOME_CHANNEL` | QQ group ID for cron delivery and notifications |
|
|
||||||
| `QQ_HOME_CHANNEL_NAME` | Display name for the QQ home channel |
|
|
||||||
|
|
||||||
| `MATTERMOST_URL` | Mattermost server URL (e.g. `https://mm.example.com`) |
|
| `MATTERMOST_URL` | Mattermost server URL (e.g. `https://mm.example.com`) |
|
||||||
| `MATTERMOST_TOKEN` | Bot token or personal access token for Mattermost |
|
| `MATTERMOST_TOKEN` | Bot token or personal access token for Mattermost |
|
||||||
| `MATTERMOST_ALLOWED_USERS` | Comma-separated Mattermost user IDs allowed to message the bot |
|
| `MATTERMOST_ALLOWED_USERS` | Comma-separated Mattermost user IDs allowed to message the bot |
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ For the full voice feature set — including CLI microphone mode, spoken replies
|
||||||
| WeCom Callback | — | — | — | — | — | — | — |
|
| WeCom Callback | — | — | — | — | — | — | — |
|
||||||
| Weixin | ✅ | ✅ | ✅ | — | — | ✅ | ✅ |
|
| Weixin | ✅ | ✅ | ✅ | — | — | ✅ | ✅ |
|
||||||
| BlueBubbles | — | ✅ | ✅ | — | ✅ | ✅ | — |
|
| BlueBubbles | — | ✅ | ✅ | — | ✅ | ✅ | — |
|
||||||
| QQ | ✅ | ✅ | ✅ | — | — | — | — |
|
| QQ | ✅ | ✅ | ✅ | — | — | ✅ | — |
|
||||||
|
|
||||||
**Voice** = TTS audio replies and/or voice message transcription. **Images** = send/receive images. **Files** = send/receive file attachments. **Threads** = threaded conversations. **Reactions** = emoji reactions on messages. **Typing** = typing indicator while processing. **Streaming** = progressive message updates via editing.
|
**Voice** = TTS audio replies and/or voice message transcription. **Images** = send/receive images. **Files** = send/receive file attachments. **Threads** = threaded conversations. **Reactions** = emoji reactions on messages. **Typing** = typing indicator while processing. **Streaming** = progressive message updates via editing.
|
||||||
|
|
||||||
|
|
@ -82,6 +82,7 @@ flowchart TB
|
||||||
wcb --> store
|
wcb --> store
|
||||||
wx --> store
|
wx --> store
|
||||||
bb --> store
|
bb --> store
|
||||||
|
qq --> store
|
||||||
api --> store
|
api --> store
|
||||||
wh --> store
|
wh --> store
|
||||||
store --> agent
|
store --> agent
|
||||||
|
|
@ -371,7 +372,7 @@ Each platform has its own toolset:
|
||||||
| WeCom Callback | `hermes-wecom-callback` | Full tools including terminal |
|
| WeCom Callback | `hermes-wecom-callback` | Full tools including terminal |
|
||||||
| Weixin | `hermes-weixin` | Full tools including terminal |
|
| Weixin | `hermes-weixin` | Full tools including terminal |
|
||||||
| BlueBubbles | `hermes-bluebubbles` | Full tools including terminal |
|
| BlueBubbles | `hermes-bluebubbles` | Full tools including terminal |
|
||||||
| QQ | `hermes-qq` | Full tools including terminal |
|
| QQBot | `hermes-qqbot` | Full tools including terminal |
|
||||||
| API Server | `hermes` (default) | Full tools including terminal |
|
| API Server | `hermes` (default) | Full tools including terminal |
|
||||||
| Webhooks | `hermes-webhook` | Full tools including terminal |
|
| Webhooks | `hermes-webhook` | Full tools including terminal |
|
||||||
|
|
||||||
|
|
@ -393,6 +394,6 @@ Each platform has its own toolset:
|
||||||
- [WeCom Callback Setup](wecom-callback.md)
|
- [WeCom Callback Setup](wecom-callback.md)
|
||||||
- [Weixin Setup (WeChat)](weixin.md)
|
- [Weixin Setup (WeChat)](weixin.md)
|
||||||
- [BlueBubbles Setup (iMessage)](bluebubbles.md)
|
- [BlueBubbles Setup (iMessage)](bluebubbles.md)
|
||||||
- [QQ Bot Setup](qq.md)
|
- [QQBot Setup](qqbot.md)
|
||||||
- [Open WebUI + API Server](open-webui.md)
|
- [Open WebUI + API Server](open-webui.md)
|
||||||
- [Webhooks](webhooks.md)
|
- [Webhooks](webhooks.md)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue