feat: add QQ Bot integration with Official API v2 and voice STT#7550
feat: add QQ Bot integration with Official API v2 and voice STT#7550topcheer wants to merge 2 commits into
Conversation
- Implement QQ Bot adapter using Official QQ Bot API v2 (WebSocket Gateway + REST API) - Voice processing: prioritize QQ's built-in asr_refer_text, then voice_wav_url, then configurable STT - Unified attachment processing (images, voice, files) for all message types (C2C/group/guild/DM) - Configurable STT backend via config.yaml or QQ_STT_* environment variables - Add QQ to hermes setup gateway, hermes gateway menu, config, status, and toolsets - Add QQ platform integration: scheduler, channel directory, send_message_tool, cron support - Add tests for QQ adapter
There was a problem hiding this comment.
Pull request overview
Adds a new QQ Bot messaging integration (Official QQ Bot API v2) to Hermes Gateway, including voice message transcription support and accompanying CLI/docs/tooling updates.
Changes:
- Introduces a new
QQAdapterthat connects to QQ’s WebSocket Gateway for inbound events and uses REST for outbound messaging/media. - Adds QQ platform wiring across gateway config/runtime, CLI setup/status/tools config, toolsets, and cron routing.
- Adds QQ documentation + environment variable reference updates and a new QQ unit test suite.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
gateway/platforms/qq.py |
New QQ platform adapter with WS gateway + REST messaging + attachment/STT handling. |
gateway/config.py |
Adds Platform.QQ, connected-platform detection, and env overrides for QQ credentials/allowlists/home channel. |
gateway/run.py |
Registers QQ adapter and updates gateway runtime behavior (also includes unrelated behavior removals/changes). |
gateway/platforms/__init__.py |
Exposes QQAdapter via package exports (and reworks adapter exports). |
tools/send_message_tool.py |
Adds QQ support to send_message tool via _send_qq(). |
toolsets.py |
Adds hermes-qq toolset and includes it in hermes-gateway. |
hermes_cli/setup.py |
Adds interactive QQ setup flow (credentials, allowlist, STT). |
hermes_cli/gateway.py |
Adds QQ platform entry and env var prompts/instructions. |
hermes_cli/config.py |
Adds QQ env vars to CLI config surface. |
hermes_cli/tools_config.py |
Adds QQ toolset mapping and enabled-platform detection. |
hermes_cli/status.py |
Displays QQ status (token/home channel). |
cron/scheduler.py |
Adds QQ to platform routing map for delivery. |
agent/prompt_builder.py |
Adds QQ platform hint text for agent prompting. |
tests/gateway/test_qq.py |
Adds unit tests for QQ adapter helpers/config parsing. |
website/docs/user-guide/messaging/qq.md |
New QQ setup guide (deps, env vars, STT behavior). |
website/docs/user-guide/messaging/index.md |
Adds QQ to comparison table, architecture diagram, toolset list, and nav links. |
website/docs/reference/environment-variables.md |
Documents QQ env vars. |
cli-config.yaml.example |
Adds QQ platform key/toolset examples. |
AGENTS.md |
Lists QQ as a supported adapter. |
.gitignore |
Ignores .ggcode/. |
Comments suppressed due to low confidence (1)
gateway/run.py:2356
/backgroundis no longer bypassing the “agent running” guard. With the current logic, a/background ...message will fall through torunning_agent.interrupt(event.text), which can disrupt an active conversation instead of launching a parallel background task. Addbackgroundto the command bypass list (similar tonew/approve/deny) so it routes to_handle_background_command()even while an agent is running.
# Resolve the command once for all early-intercept checks below.
from hermes_cli.commands import resolve_command as _resolve_cmd_inner
_evt_cmd = event.get_command()
_cmd_def_inner = _resolve_cmd_inner(_evt_cmd) if _evt_cmd else None
if _cmd_def_inner and _cmd_def_inner.name == "restart":
return await self._handle_restart_command(event)
# /stop must hard-kill the session when an agent is running.
# A soft interrupt (agent.interrupt()) doesn't help when the agent
# is truly hung — the executor thread is blocked and never checks
# _interrupt_requested. Force-clean _running_agents so the session
# is unlocked and subsequent messages are processed normally.
if _cmd_def_inner and _cmd_def_inner.name == "stop":
running_agent = self._running_agents.get(_quick_key)
if running_agent and running_agent is not _AGENT_PENDING_SENTINEL:
running_agent.interrupt("Stop requested")
# Force-clean: remove the session lock regardless of agent state
adapter = self.adapters.get(source.platform)
if adapter and hasattr(adapter, 'get_pending_message'):
adapter.get_pending_message(_quick_key) # consume and discard
self._pending_messages.pop(_quick_key, None)
if _quick_key in self._running_agents:
del self._running_agents[_quick_key]
logger.info("HARD STOP for session %s — session lock released", _quick_key[:20])
return "⚡ Force-stopped. The session is unlocked — you can send a new message."
# /reset and /new must bypass the running-agent guard so they
# actually dispatch as commands instead of being queued as user
# text (which would be fed back to the agent with the same
# broken history — #2170). Interrupt the agent first, then
# clear the adapter's pending queue so the stale "/reset" text
# doesn't get re-processed as a user message after the
# interrupt completes.
if _cmd_def_inner and _cmd_def_inner.name == "new":
running_agent = self._running_agents.get(_quick_key)
if running_agent and running_agent is not _AGENT_PENDING_SENTINEL:
running_agent.interrupt("Session reset requested")
# Clear any pending messages so the old text doesn't replay
adapter = self.adapters.get(source.platform)
if adapter and hasattr(adapter, 'get_pending_message'):
adapter.get_pending_message(_quick_key) # consume and discard
self._pending_messages.pop(_quick_key, None)
# Clean up the running agent entry so the reset handler
# doesn't think an agent is still active.
if _quick_key in self._running_agents:
del self._running_agents[_quick_key]
return await self._handle_reset_command(event)
# /queue <prompt> — queue without interrupting
if event.get_command() in ("queue", "q"):
queued_text = event.get_command_args().strip()
if not queued_text:
return "Usage: /queue <prompt>"
adapter = self.adapters.get(source.platform)
if adapter:
from gateway.platforms.base import MessageEvent as _ME, MessageType as _MT
queued_event = _ME(
text=queued_text,
message_type=_MT.TEXT,
source=event.source,
message_id=event.message_id,
)
adapter._pending_messages[_quick_key] = queued_event
return "Queued for the next turn."
# /model must not be used while the agent is running.
if _cmd_def_inner and _cmd_def_inner.name == "model":
return "Agent is running — wait or /stop first, then switch models."
# /approve and /deny must bypass the running-agent interrupt path.
# The agent thread is blocked on a threading.Event inside
# tools/approval.py — sending an interrupt won't unblock it.
# Route directly to the approval handler so the event is signalled.
if _cmd_def_inner and _cmd_def_inner.name in ("approve", "deny"):
if _cmd_def_inner.name == "approve":
return await self._handle_approve_command(event)
return await self._handle_deny_command(event)
if event.message_type == MessageType.PHOTO:
logger.debug("PRIORITY photo follow-up for session %s — queueing without interrupt", _quick_key[:20])
adapter = self.adapters.get(source.platform)
if adapter:
merge_pending_message_event(adapter._pending_messages, _quick_key, event)
return None
running_agent = self._running_agents.get(_quick_key)
if running_agent is _AGENT_PENDING_SENTINEL:
# Agent is being set up but not ready yet.
if event.get_command() == "stop":
# Force-clean the sentinel so the session is unlocked.
if _quick_key in self._running_agents:
del self._running_agents[_quick_key]
logger.info("HARD STOP (pending) for session %s — sentinel cleared", _quick_key[:20])
return "⚡ Force-stopped. The agent was still starting — session unlocked."
# Queue the message so it will be picked up after the
# agent starts.
adapter = self.adapters.get(source.platform)
if adapter:
adapter._pending_messages[_quick_key] = event
return None
if self._draining:
if self._queue_during_drain_enabled():
self._queue_or_replace_pending_event(_quick_key, event)
return (
f"⏳ Gateway {self._status_action_gerund()} — queued for the next turn after it comes back."
if self._queue_during_drain_enabled()
else f"⏳ Gateway is {self._status_action_gerund()} and is not accepting another turn right now."
)
logger.debug("PRIORITY interrupt for session %s", _quick_key[:20])
running_agent.interrupt(event.text)
if _quick_key in self._pending_messages:
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| elif platform == Platform.BLUEBUBBLES: | ||
| from gateway.platforms.bluebubbles import BlueBubblesAdapter, check_bluebubbles_requirements | ||
| if not check_bluebubbles_requirements(): | ||
| logger.warning("BlueBubbles: aiohttp/httpx missing or BLUEBUBBLES_SERVER_URL/BLUEBUBBLES_PASSWORD not configured") | ||
| return None | ||
| return BlueBubblesAdapter(config) | ||
|
|
||
| elif platform == Platform.QQ: | ||
| from gateway.platforms.qq import QQAdapter, check_qq_requirements | ||
| if not check_qq_requirements(): | ||
| logger.warning("QQ: aiohttp/httpx not installed. Run: pip install aiohttp httpx") | ||
| return None | ||
| return QQAdapter(config) | ||
|
|
There was a problem hiding this comment.
_create_adapter() no longer has a Platform.WEIXIN branch, even though Platform.WEIXIN still exists (and gateway/config.py still configures Weixin). This looks like an accidental regression that would prevent the Weixin adapter from ever starting. Re-add the Weixin adapter creation case (and its dependency check) or remove Weixin support consistently across the codebase if that was intentional.
| platform_env_map = { | ||
| Platform.TELEGRAM: "TELEGRAM_ALLOWED_USERS", | ||
| Platform.DISCORD: "DISCORD_ALLOWED_USERS", | ||
| Platform.WHATSAPP: "WHATSAPP_ALLOWED_USERS", | ||
| Platform.SLACK: "SLACK_ALLOWED_USERS", | ||
| Platform.SIGNAL: "SIGNAL_ALLOWED_USERS", | ||
| Platform.EMAIL: "EMAIL_ALLOWED_USERS", | ||
| Platform.SMS: "SMS_ALLOWED_USERS", | ||
| Platform.MATTERMOST: "MATTERMOST_ALLOWED_USERS", | ||
| Platform.MATRIX: "MATRIX_ALLOWED_USERS", | ||
| Platform.DINGTALK: "DINGTALK_ALLOWED_USERS", | ||
| Platform.FEISHU: "FEISHU_ALLOWED_USERS", | ||
| Platform.WECOM: "WECOM_ALLOWED_USERS", | ||
| Platform.WEIXIN: "WEIXIN_ALLOWED_USERS", | ||
| Platform.BLUEBUBBLES: "BLUEBUBBLES_ALLOWED_USERS", | ||
| Platform.QQ: "QQ_ALLOWED_USERS", | ||
| } | ||
| platform_allow_all_map = { | ||
| Platform.TELEGRAM: "TELEGRAM_ALLOW_ALL_USERS", | ||
| Platform.DISCORD: "DISCORD_ALLOW_ALL_USERS", | ||
| Platform.WHATSAPP: "WHATSAPP_ALLOW_ALL_USERS", | ||
| Platform.SLACK: "SLACK_ALLOW_ALL_USERS", | ||
| Platform.SIGNAL: "SIGNAL_ALLOW_ALL_USERS", | ||
| Platform.EMAIL: "EMAIL_ALLOW_ALL_USERS", | ||
| Platform.SMS: "SMS_ALLOW_ALL_USERS", | ||
| Platform.MATTERMOST: "MATTERMOST_ALLOW_ALL_USERS", | ||
| Platform.MATRIX: "MATRIX_ALLOW_ALL_USERS", | ||
| Platform.DINGTALK: "DINGTALK_ALLOW_ALL_USERS", | ||
| Platform.FEISHU: "FEISHU_ALLOW_ALL_USERS", | ||
| Platform.WECOM: "WECOM_ALLOW_ALL_USERS", | ||
| Platform.WEIXIN: "WEIXIN_ALLOW_ALL_USERS", | ||
| Platform.BLUEBUBBLES: "BLUEBUBBLES_ALLOW_ALL_USERS", | ||
| Platform.QQ: "QQ_ALLOW_ALL_USERS", | ||
| } |
There was a problem hiding this comment.
The authorization env-var maps drop Platform.WEIXIN entirely, which means Weixin users can no longer be authorized via WEIXIN_ALLOWED_USERS/WEIXIN_ALLOW_ALL_USERS. Also, QQ_ALLOW_ALL_USERS is referenced but isn’t documented/handled in setup/config like other *_ALLOW_ALL_USERS flags. Please restore Weixin entries (or remove Weixin end-to-end) and either add QQ allow-all support consistently (docs + config) or remove this unused flag.
| "FEISHU_ALLOWED_USERS", | ||
| "WECOM_ALLOWED_USERS", |
There was a problem hiding this comment.
The “no allowlists configured” warning doesn’t consider QQ (and no longer considers Weixin). If a user sets QQ_ALLOWED_USERS (or WEIXIN_ALLOWED_USERS), _any_allowlist stays false and the gateway logs a misleading warning. Include the QQ/Weixin allowlist env vars in this check so the warning reflects actual configuration.
| "FEISHU_ALLOWED_USERS", | |
| "WECOM_ALLOWED_USERS", | |
| "FEISHU_ALLOWED_USERS", "QQ_ALLOWED_USERS", | |
| "WEIXIN_ALLOWED_USERS", "WECOM_ALLOWED_USERS", |
| """Handle /yolo — toggle dangerous command approval bypass.""" | ||
| current = bool(os.environ.get("HERMES_YOLO_MODE")) | ||
| if current: | ||
| disable_session_yolo(session_key) | ||
| return "⚠️ YOLO mode **OFF** for this session — dangerous commands will require approval." | ||
| os.environ.pop("HERMES_YOLO_MODE", None) | ||
| return "⚠️ YOLO mode **OFF** — dangerous commands will require approval." | ||
| else: | ||
| enable_session_yolo(session_key) | ||
| return "⚡ YOLO mode **ON** for this session — all commands auto-approved. Use with caution." | ||
| os.environ["HERMES_YOLO_MODE"] = "1" | ||
| return "⚡ YOLO mode **ON** — all commands auto-approved. Use with caution." |
There was a problem hiding this comment.
/yolo is now toggling the process-wide HERMES_YOLO_MODE env var, which changes behavior from session-scoped to global. This both weakens safety (anyone can enable approval bypass for all sessions) and contradicts existing semantics in tools/approval.py and tests like tests/gateway/test_yolo_command.py (which expect session-scoped toggling). Please revert to using the session-scoped enable/disable helpers (enable_session_yolo / disable_session_yolo) and keep HERMES_YOLO_MODE reserved for the CLI --yolo flag.
| pr = self._provider_routing | ||
| reasoning_config = self._load_reasoning_config() | ||
| self._reasoning_config = reasoning_config | ||
| self._service_tier = self._load_service_tier() | ||
| # Set up streaming consumer if enabled | ||
| _stream_consumer = None | ||
| _stream_delta_cb = None |
There was a problem hiding this comment.
Per-session /model overrides are stored in self._session_model_overrides, but the agent run path here builds model + runtime_kwargs and proceeds to turn_route = ... without applying any session override. This likely makes /model have no effect on subsequent turns. Apply the stored override before resolving turn_route (and keep /new clearing it as you already do).
| return None | ||
| return BlueBubblesAdapter(config) | ||
|
|
||
| elif platform == Platform.QQ: | ||
| from gateway.platforms.qq import QQAdapter, check_qq_requirements | ||
| if not check_qq_requirements(): | ||
| logger.warning("QQ: aiohttp/httpx not installed. Run: pip install aiohttp httpx") | ||
| return None | ||
| return QQAdapter(config) | ||
|
|
||
| return None | ||
|
|
||
| def _is_user_authorized(self, source: SessionSource) -> bool: |
There was a problem hiding this comment.
This PR is titled/descrbed as adding QQ integration, but gateway/run.py changes also remove or significantly alter unrelated features (e.g., Weixin adapter creation/authorization, /yolo semantics, /background bypass behavior, /fast command removal, usage reporting changes). If these behavior changes are intentional, they should be called out explicitly in the PR description; otherwise they should be reverted/split into a separate PR to keep the QQ integration reviewable.
| @@ -828,19 +832,7 @@ | |||
| "args": list(runtime_kwargs.get("args") or []), | |||
| "credential_pool": runtime_kwargs.get("credential_pool"), | |||
| } | |||
There was a problem hiding this comment.
This change removes the service_tier / “Priority Processing” routing behavior from _resolve_turn_agent_config() (it now just returns resolve_turn_route(...) without any request overrides). The codebase still has tests and CLI behaviors around /fast / agent.service_tier (see tests/gateway/test_fast_command.py), so this will either break existing functionality or require coordinated updates elsewhere. If Priority Processing support is still intended, restore the override injection; otherwise remove/update the related command/tests/docs in the same PR.
| } | |
| } | |
| service_tier = runtime_kwargs.get("service_tier") | |
| if service_tier: | |
| primary["request_overrides"] = {"service_tier": service_tier} |
| @@ -8102,10 +7892,10 @@ | |||
| existing_pid, | |||
| ) | |||
| try: | |||
| terminate_pid(existing_pid, force=False) | |||
| os.kill(existing_pid, signal.SIGTERM) | |||
| except ProcessLookupError: | |||
| pass # Already gone | |||
| except (PermissionError, OSError): | |||
| except PermissionError: | |||
There was a problem hiding this comment.
start_gateway(..., replace=True) used to call gateway.status.terminate_pid(), which has Windows-specific behavior (e.g., taskkill /T /F for force-kill). Replacing it with os.kill(..., SIGTERM/SIGKILL) can break --replace behavior on Windows and may not terminate child processes. Prefer keeping terminate_pid() (or replicate its cross-platform semantics here).
| async def _handle_usage_command(self, event: MessageEvent) -> str: | ||
| """Handle /usage command -- show token usage for the current session. | ||
|
|
||
| Checks both _running_agents (mid-turn) and _agent_cache (between turns) | ||
| so that rate limits, cost estimates, and detailed token breakdowns are | ||
| available whenever the user asks, not only while the agent is running. | ||
| """ | ||
| """Handle /usage command -- show token usage for the session's last agent run.""" | ||
| source = event.source | ||
| session_key = self._session_key_for_source(source) | ||
|
|
||
| # Try running agent first (mid-turn), then cached agent (between turns) | ||
| agent = self._running_agents.get(session_key) | ||
| if not agent or agent is _AGENT_PENDING_SENTINEL: | ||
| _cache_lock = getattr(self, "_agent_cache_lock", None) | ||
| _cache = getattr(self, "_agent_cache", None) | ||
| if _cache_lock and _cache is not None: | ||
| with _cache_lock: | ||
| cached = _cache.get(session_key) | ||
| if cached: | ||
| agent = cached[0] | ||
|
|
||
| if agent and hasattr(agent, "session_total_tokens") and agent.session_api_calls > 0: | ||
| lines = [] |
There was a problem hiding this comment.
_handle_usage_command() no longer checks self._agent_cache when there’s no running agent (the cache lookup block was removed). This regresses the documented behavior (“/usage should work between turns”) and will likely break tests/gateway/test_usage_command.py (which asserts cached-agent usage is shown). Please restore the cache fallback (and any related fields like cost/token breakdown if still desired).
| try: | ||
| self._http_client = httpx.AsyncClient(timeout=30.0, follow_redirects=True) | ||
|
|
There was a problem hiding this comment.
httpx.AsyncClient is created with follow_redirects=True, and downloads rely on a single pre-flight is_safe_url(url) check. This can be bypassed via redirect-based SSRF (public URL → 302 → private IP), which gateway/platforms/base.py explicitly guards against via _ssrf_redirect_guard event hooks. Consider adding event_hooks={"response": [_ssrf_redirect_guard]} (or an equivalent redirect re-check) when constructing _http_client, and avoid passing follow_redirects=True per-request without a redirect guard.
|
Hi @topcheer 👋 I'm from the official QQ Bot team at Tencent. We noticed this PR — a QQ Bot adapter with API v2 integration and voice STT support, really impressive scope. We saw the PR was closed — is this still something you're planning to revisit? We'd love to help out if so. Whether it's narrowing the diff, resolving the review feedback, or anything SDK-related, happy to assist. Feel free to leave your QQ number if you'd like to connect directly! |
Hi @WideLee, Most of the logic was from official qq-bot for openclaw. I made this for own use in the first place and then thought it’s good to share with anyone might need this hence the PR. But something went wrong with the source code later I decide to delete the PR and created a new one. #7616 My QQ: 1,0,7,5.3,1,3 Cheers |
What does this PR do?
Adds complete QQ Bot integration using the Official QQ Bot API v2. The adapter connects to QQ's WebSocket Gateway for inbound events and uses the REST API (
api.sgroup.qq.com) for outbound messages and media uploads.The implementation follows the same architecture as the OpenClaw qqbot plugin (
@openclaw/qqbot), the most battle-tested QQ Bot adapter in the ecosystem.Related Issue
N/A (new platform integration)
Type of Change
Changes Made
Core adapter
gateway/platforms/qq.py— NewQQAdapter(~1785 lines) using Official QQ Bot API v2gateway/platforms/__init__.py— QQAdapter import/exportgateway/config.py—Platform.QQenum + env override for Official API v2 credentialsgateway/run.py— QQ adapter registration in_get_adapter_for_platform()Voice STT
_process_attachments()— unified attachment processing for all message types (C2C/group/guild/DM)_stt_voice_attachment()— 3-tier fallback: QQasr_refer_text→voice_wav_url→ configurable STT_resolve_stt_config()/_call_stt()— configurable STT backend (OpenAI-compatible API)Platform integration
hermes_cli/setup.py— new_setup_qq()+_GATEWAY_PLATFORMSregistration +any_messaging+missing_homehermes_cli/gateway.py— QQ_PLATFORMSentry with STT configuration varshermes_cli/config.py—QQ_STT_*/QQ_ALLOWED_USERS/QQ_HOME_CHANNELenv varshermes_cli/status.py— QQ status displayhermes_cli/tools_config.py— QQ toolset configtools/send_message_tool.py— QQ send_message supporttoolsets.py—hermes-gatewayincludeshermes-qqcron/scheduler.py— QQ platform mapgateway/channel_directory.py— channel directory QQ supportagent/prompt_builder.py— QQ system promptTests
tests/gateway/test_qq.py— 27 unit testsDocumentation
website/docs/user-guide/messaging/qq.md— new QQ Bot setup guidewebsite/docs/user-guide/messaging/index.md— QQ added to comparison table, architecture diagram, toolsets table, nav linkswebsite/docs/reference/environment-variables.md— QQ env vars documentedcli-config.yaml.example— QQ added to supported platform keys and defaultsAGENTS.md— QQ added to platforms listHow to Test
hermes setup gateway— select QQ, enter App ID + App Secret from q.qq.comhermes gateway runpython -m pytest tests/gateway/test_qq.py -q— 27 tests passChecklist
Code
feat: add QQ Bot integration with Official API v2 and voice STT)pytest tests/gateway/test_qq.py -qand all tests pass (27 passed)Documentation & Housekeeping
docs/, docstrings)website/docs/user-guide/messaging/qq.md— new setup guidewebsite/docs/user-guide/messaging/index.md— comparison table, architecture, toolsets, navwebsite/docs/reference/environment-variables.md— QQ env varscli-config.yaml.example— QQ added to supported platform keys and defaultsAGENTS.md— QQ added to platforms list~/.hermessend_message)For New Skills
N/A (platform adapter, not a skill)
cat PR_DESCRIPTION.md
What does this PR do?
Adds complete QQ Bot integration using the Official QQ Bot API v2. The adapter connects to QQ's WebSocket Gateway for inbound events and uses the REST API (
api.sgroup.qq.com) for outbound messages and media uploads.The implementation follows the same architecture as the OpenClaw qqbot plugin (
@openclaw/qqbot), the most battle-tested QQ Bot adapter in the ecosystem.Related Issue
N/A (new platform integration)
Type of Change
Changes Made
Core adapter
gateway/platforms/qq.py— NewQQAdapter(~1898 lines) using Official QQ Bot API v2gateway/platforms/__init__.py— QQAdapter import/exportgateway/config.py—Platform.QQenum + env override + connected platform detectiongateway/run.py— QQ adapter registration + QQ-specific dep warningVoice STT
_process_attachments()— unified attachment processing for all message types (C2C/group/guild/DM)_stt_voice_attachment()— 3-tier fallback: QQasr_refer_text→voice_wav_url→ configurable STT_resolve_stt_config()/_call_stt()— configurable STT backend (OpenAI-compatible API)WebSocket keep-alive
heartbeat_interval(× 0.8), not a hardcoded valueQQCloseErrorwith close code and reason for diagnosable logsPlatform integration
hermes_cli/setup.py— new_setup_qq()+_GATEWAY_PLATFORMSregistration +any_messaging+missing_homehermes_cli/gateway.py— QQ_PLATFORMSentry with STT configuration varshermes_cli/config.py—QQ_STT_*/QQ_ALLOWED_USERS/QQ_HOME_CHANNELenv varshermes_cli/status.py— QQ status displayhermes_cli/tools_config.py— QQ toolset configtools/send_message_tool.py— QQ send_message supporttoolsets.py—hermes-gatewayincludeshermes-qqcron/scheduler.py— QQ platform mapgateway/channel_directory.py— channel directory QQ supportagent/prompt_builder.py— QQ system promptTests
tests/gateway/test_qq.py— 27 unit testsDocumentation
website/docs/user-guide/messaging/qq.md— new QQ Bot setup guidewebsite/docs/user-guide/messaging/index.md— QQ added to comparison table, architecture diagram, toolsets table, nav linkswebsite/docs/reference/environment-variables.md— QQ env vars documentedcli-config.yaml.example— QQ added to supported platform keys and defaultsAGENTS.md— QQ added to platforms listHow to Test
hermes setup gateway— select QQ, enter App ID + App Secret from q.qq.comhermes gateway runpython -m pytest tests/gateway/test_qq.py -q— 27 tests passChecklist
Code
feat: add QQ Bot integration with Official API v2 and voice STT)pytest tests/gateway/test_qq.py -qand all tests pass (27 passed)Documentation & Housekeeping
docs/, docstrings)website/docs/user-guide/messaging/qq.md— new setup guidewebsite/docs/user-guide/messaging/index.md— comparison table, architecture, toolsets, navwebsite/docs/reference/environment-variables.md— QQ env varscli-config.yaml.example— QQ added to supported platform keys and defaultsAGENTS.md— QQ added to platforms list~/.hermessend_message)For New Skills
N/A (platform adapter, not a skill)