Skip to content

feat: add QQ Bot integration with Official API v2 and voice STT#7550

Closed
topcheer wants to merge 2 commits into
NousResearch:mainfrom
topcheer:main
Closed

feat: add QQ Bot integration with Official API v2 and voice STT#7550
topcheer wants to merge 2 commits into
NousResearch:mainfrom
topcheer:main

Conversation

@topcheer

Copy link
Copy Markdown

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

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

Core adapter

  • gateway/platforms/qq.py — New QQAdapter (~1785 lines) using Official QQ Bot API v2
  • gateway/platforms/__init__.py — QQAdapter import/export
  • gateway/config.pyPlatform.QQ enum + env override for Official API v2 credentials
  • gateway/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: QQ asr_refer_textvoice_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_PLATFORMS registration + any_messaging + missing_home
  • hermes_cli/gateway.py — QQ _PLATFORMS entry with STT configuration vars
  • hermes_cli/config.pyQQ_STT_* / QQ_ALLOWED_USERS / QQ_HOME_CHANNEL env vars
  • hermes_cli/status.py — QQ status display
  • hermes_cli/tools_config.py — QQ toolset config
  • tools/send_message_tool.py — QQ send_message support
  • toolsets.pyhermes-gateway includes hermes-qq
  • cron/scheduler.py — QQ platform map
  • gateway/channel_directory.py — channel directory QQ support
  • agent/prompt_builder.py — QQ system prompt

Tests

  • tests/gateway/test_qq.py — 27 unit tests

Documentation

  • website/docs/user-guide/messaging/qq.md — new QQ Bot setup guide
  • website/docs/user-guide/messaging/index.md — QQ added to comparison table, architecture diagram, toolsets table, nav links
  • website/docs/reference/environment-variables.md — QQ env vars documented
  • cli-config.yaml.example — QQ added to supported platform keys and defaults
  • AGENTS.md — QQ added to platforms list

How to Test

  1. hermes setup gateway — select QQ, enter App ID + App Secret from q.qq.com
  2. Verify the bot connects via WebSocket Gateway: hermes gateway run
  3. Send a voice message in a QQ DM — verify it's transcribed using QQ's built-in ASR (no extra config)
  4. Send a text message in a group with @mention — verify the bot responds
  5. python -m pytest tests/gateway/test_qq.py -q — 27 tests pass

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat: add QQ Bot integration with Official API v2 and voice STT)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/gateway/test_qq.py -q and all tests pass (27 passed)
  • I've added tests for my changes (27 tests covering init, ACL, voice detection, STT config, message type detection)
  • I've tested on my platform: macOS

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings)
    • website/docs/user-guide/messaging/qq.md — new setup guide
    • website/docs/user-guide/messaging/index.md — comparison table, architecture, toolsets, nav
    • website/docs/reference/environment-variables.md — QQ env vars
  • I've updated cli-config.yaml.example — QQ added to supported platform keys and defaults
  • I've updated AGENTS.md — QQ added to platforms list
  • I've considered cross-platform impact — no platform-specific paths, no hardcoded ~/.hermes
  • I've updated tool descriptions/schemas — N/A (no new tools, uses existing send_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

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

Core adapter

  • gateway/platforms/qq.py — New QQAdapter (~1898 lines) using Official QQ Bot API v2
  • gateway/platforms/__init__.py — QQAdapter import/export
  • gateway/config.pyPlatform.QQ enum + env override + connected platform detection
  • gateway/run.py — QQ adapter registration + QQ-specific dep warning

Voice STT

  • _process_attachments() — unified attachment processing for all message types (C2C/group/guild/DM)
  • _stt_voice_attachment() — 3-tier fallback: QQ asr_refer_textvoice_wav_url → configurable STT
  • _resolve_stt_config() / _call_stt() — configurable STT backend (OpenAI-compatible API)

WebSocket keep-alive

  • Heartbeat interval uses server-provided heartbeat_interval (× 0.8), not a hardcoded value
  • Close code handling following the OpenClaw qqbot reference: 4004 (token refresh), 4006/4007/4009 (session reset), 4008 (rate limit backoff), 4914/4915 (fatal stop)
  • Quick disconnect detection (3× within 5s → stop and warn about permissions)
  • QQCloseError with close code and reason for diagnosable logs

Platform integration

  • hermes_cli/setup.py — new _setup_qq() + _GATEWAY_PLATFORMS registration + any_messaging + missing_home
  • hermes_cli/gateway.py — QQ _PLATFORMS entry with STT configuration vars
  • hermes_cli/config.pyQQ_STT_* / QQ_ALLOWED_USERS / QQ_HOME_CHANNEL env vars
  • hermes_cli/status.py — QQ status display
  • hermes_cli/tools_config.py — QQ toolset config
  • tools/send_message_tool.py — QQ send_message support
  • toolsets.pyhermes-gateway includes hermes-qq
  • cron/scheduler.py — QQ platform map
  • gateway/channel_directory.py — channel directory QQ support
  • agent/prompt_builder.py — QQ system prompt

Tests

  • tests/gateway/test_qq.py — 27 unit tests

Documentation

  • website/docs/user-guide/messaging/qq.md — new QQ Bot setup guide
  • website/docs/user-guide/messaging/index.md — QQ added to comparison table, architecture diagram, toolsets table, nav links
  • website/docs/reference/environment-variables.md — QQ env vars documented
  • cli-config.yaml.example — QQ added to supported platform keys and defaults
  • AGENTS.md — QQ added to platforms list

How to Test

  1. hermes setup gateway — select QQ, enter App ID + App Secret from q.qq.com
  2. Verify the bot connects via WebSocket Gateway: hermes gateway run
  3. Send a voice message in a QQ DM — verify it's transcribed using QQ's built-in ASR (no extra config)
  4. Send a text message in a group with @mention — verify the bot responds
  5. python -m pytest tests/gateway/test_qq.py -q — 27 tests pass

Checklist

Code

  • [x ] I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat: add QQ Bot integration with Official API v2 and voice STT)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/gateway/test_qq.py -q and all tests pass (27 passed)
  • I've added tests for my changes (27 tests covering init, ACL, voice detection, STT config, message type detection)
  • I've tested on my platform: macOS

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings)
    • website/docs/user-guide/messaging/qq.md — new setup guide
    • website/docs/user-guide/messaging/index.md — comparison table, architecture, toolsets, nav
    • website/docs/reference/environment-variables.md — QQ env vars
  • I've updated cli-config.yaml.example — QQ added to supported platform keys and defaults
  • I've updated AGENTS.md — QQ added to platforms list
  • I've considered cross-platform impact — no platform-specific paths, no hardcoded ~/.hermes
  • I've updated tool descriptions/schemas — N/A (no new tools, uses existing send_message)

For New Skills

N/A (platform adapter, not a skill)

- 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
Copilot AI review requested due to automatic review settings April 11, 2026 05:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 QQAdapter that 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

  • /background is no longer bypassing the “agent running” guard. With the current logic, a /background ... message will fall through to running_agent.interrupt(event.text), which can disrupt an active conversation instead of launching a parallel background task. Add background to the command bypass list (similar to new/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.

Comment thread gateway/run.py
Comment on lines 1973 to +1986
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)

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Copilot uses AI. Check for mistakes.
Comment thread gateway/run.py
Comment on lines 2012 to 2043
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",
}

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread gateway/run.py
Comment on lines 1330 to 1331
"FEISHU_ALLOWED_USERS",
"WECOM_ALLOWED_USERS",

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"FEISHU_ALLOWED_USERS",
"WECOM_ALLOWED_USERS",
"FEISHU_ALLOWED_USERS", "QQ_ALLOWED_USERS",
"WEIXIN_ALLOWED_USERS", "WECOM_ALLOWED_USERS",

Copilot uses AI. Check for mistakes.
Comment thread gateway/run.py
Comment on lines +5273 to +5280
"""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."

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/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.

Copilot uses AI. Check for mistakes.
Comment thread gateway/run.py
Comment on lines 7033 to 7038
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

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread gateway/run.py
Comment on lines 1977 to 1989
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:

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread gateway/run.py
@@ -828,19 +832,7 @@
"args": list(runtime_kwargs.get("args") or []),
"credential_pool": runtime_kwargs.get("credential_pool"),
}

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
}
}
service_tier = runtime_kwargs.get("service_tier")
if service_tier:
primary["request_overrides"] = {"service_tier": service_tier}

Copilot uses AI. Check for mistakes.
Comment thread gateway/run.py
Comment on lines 7883 to +7898
@@ -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:

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread gateway/run.py
Comment on lines 5639 to 5646
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 = []

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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).

Copilot uses AI. Check for mistakes.
Comment thread gateway/platforms/qq.py
Comment on lines +214 to +216
try:
self._http_client = httpx.AsyncClient(timeout=30.0, follow_redirects=True)

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@topcheer topcheer closed this by deleting the head repository Apr 11, 2026
@WideLee

WideLee commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

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!

@topcheer

Copy link
Copy Markdown
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants