Skip to content

refactor(gateway): migrate QQBot adapter to bundled plugin - #41065

Closed
kshitijk4poor wants to merge 2 commits into
NousResearch:mainfrom
kshitijk4poor:feat/qqbot-platform-plugin
Closed

kshitijk4poor wants to merge 2 commits into
NousResearch:mainfrom
kshitijk4poor:feat/qqbot-platform-plugin

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Contributor

What this does

Relocates the QQBot adapter package from gateway/platforms/qqbot/ into a
self-contained bundled plugin under plugins/platforms/qqbot/, following the
Discord / Mattermost / Home Assistant / Yuanbao / Signal migration shape.
QQBot was already a package (adapter + constants + utils + crypto + onboard +
keyboards + chunked_upload), so this is mostly a directory relocate plus the
register() block; intra-package imports switch from absolute to relative.

How

register() supplies the hooks that were per-platform wiring in core:
adapter_factory, check_fn, is_connected, setup_fn (QR scan-to-configure +
manual entry), standalone_sender_fn, cron_deliver_env_var.

The REST sender (token exchange + channel/C2C/group endpoint fallback) moved
into the plugin as _standalone_send; tools/send_message_tool.py keeps a
thin _send_qqbot shim so the in-module dispatch is unchanged. QQBot is HTTP,
so cron delivery works out-of-process.

Out of scope (stays generic, same as every other platform)

Platform.QQBOT enum literal, the _apply_env_overrides QQ_/QQBOT_ env
block, the _is_user_authorized allowlist maps. QQBot has no
load_gateway_config YAML block, so no apply_yaml_config_fn. The
gateway.platforms.QQAdapter re-export is repointed to the plugin path.

Tests

8 clean renames (R091 adapter + R098–R100 on the package files). 357 focused
tests pass. Updated the connection-checker guard test to exclude bundled-plugin
platforms whose is_connected lives in the plugin. No new failures vs main
(pre-existing matrix/telegram xdist flakes excluded).

Relocate the QQBot adapter package from gateway/platforms/qqbot/ into a
self-contained bundled plugin under plugins/platforms/qqbot/, following the
Discord/Mattermost/Home Assistant/Yuanbao/Signal migration shape. QQBot was
already a package (adapter + constants + utils + crypto + onboard + keyboards
+ chunked_upload), so this is mostly a directory relocate plus the register()
block; intra-package imports switch from absolute to relative.

register() supplies the hooks that previously lived as per-platform wiring
in core:
  - adapter_factory      -> the elif in gateway/run.py::_create_adapter()
  - check_fn             -> check_qq_requirements guard there
  - is_connected         -> the Platform.QQBOT lambda in gateway/config.py
  - setup_fn             -> _setup_qqbot (QR scan-to-configure + manual entry)
                            + its _PLATFORMS entry + _builtin_setup_fn mapping
                            + the delegating wrapper in hermes_cli/setup.py
  - standalone_sender_fn -> _send_qqbot in tools/send_message_tool.py
  - cron_deliver_env_var -> QQBOT_HOME_CHANNEL

The REST sender (token exchange + channel/C2C/group endpoint fallback) moved
into the plugin as _standalone_send; tools/send_message_tool.py keeps a thin
_send_qqbot shim so the in-module dispatch is unchanged. QQBot is HTTP, so cron
delivery works out-of-process.

Deliberately left generic in core: the Platform.QQBOT enum literal, the
_apply_env_overrides QQ_*/QQBOT_* env block, and the _is_user_authorized
allowlist maps. QQBot has no load_gateway_config YAML block, so no
apply_yaml_config_fn. The gateway.platforms.QQAdapter re-export is repointed
to the plugin path for back-compat. Updated the connection-checker guard test
to exclude bundled-plugin platforms whose is_connected lives in the plugin.
@alt-glitch alt-glitch added type/refactor Code restructuring, no behavior change P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery platform/qqbot QQ Bot adapter comp/plugins Plugin system and bundled plugins labels Jun 7, 2026
qqbot is now a bundled plugin; its PlatformEntry already permits /update via
allow_update_command (defaults True), honored by the registry fallback in
_handle_update_command. The hardcoded Platform.QQBOT entry in
_UPDATE_ALLOWED_PLATFORMS is therefore redundant. Same cleanup as NousResearch#32525
did for Discord/Mattermost.
@kshitijk4poor

Copy link
Copy Markdown
Contributor Author

Follow-up to #32525: now that the migrated-platform /update allowlist cleanup landed, this PR drops Platform.QQBOT from _UPDATE_ALLOWED_PLATFORMS. As a bundled plugin, qqbot's PlatformEntry.allow_update_command (default True) is already honored by the registry fallback in _handle_update_command, so the hardcoded frozenset entry is redundant. No behavior change — /update from a qqbot chat still works; test_update_command.py passes (35/35).

@cadezhou

cadezhou commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

hello ser~ Why was this PR closed? Was it because it hadn't been merged for a long time, or was there another reason? @kshitijk4poor

k176060444-lgtm added a commit to k176060444-lgtm/hermes-agent that referenced this pull request Sep 1, 2026
Move QQBot outbound REST protocol out of tools/send_message_tool.py and
into the platform package (gateway/platforms/qqbot/), where the live
adapter already owns authentication, target resolution, and chunked
upload.  The tool layer now delegates to _send_via_adapter which
prefers the in-process QQAdapter (live) and falls back to the
standalone_sender_fn registered on QQBot's PlatformEntry.

Key changes:

- NEW: gateway/platforms/qqbot/standalone.py
  Standalone QQBot sender sharing ChunkedUploader, constants, and token
  acquisition with the live adapter.  Supports text, image, voice,
  video, document, multi-attachment, force_document, and explicit
  C2C/group/guild targets.  No WebSocket — uses a temporary httpx
  client with try/finally.

- gateway/platforms/qqbot/__init__.py
  Register QQBot in platform_registry with standalone_sender_fn so
  _send_via_adapter can find it when no live adapter is present.

- tools/send_message_tool.py
  REMOVED: _QQBOT_* constants, 7 MB base64 limit, _resolve_qqbot_target,
  _send_qqbot, _send_qqbot_with_media, _is_endpoint_mismatch,
  _split_text (~400 lines).  QQBot now dispatches through
  _send_via_adapter for both text and media, matching the pattern
  used by plugin platforms (Discord, Google Chat, IRC, etc.).

- tests/tools/test_send_message_tool.py
  REMOVED: ~1330 lines of tests covering deleted tool-layer functions.

- NEW: tests/tools/test_send_message_tool_qqbot_rectify.py
  20 tests covering live-adapter priority, no-standalone-fallback,
  standalone invocation, force_document/[[as_document]] passthrough,
  target prefix resolution, and file-type classification.

Test results (excluding 6 pre-existing TestSendViaAdapterStandaloneFallback
failures unrelated to this change):
  127 existing tests pass, 0 regressions
  20 new tests pass

Maintainer review: NousResearch#40457 (teknium1)
Related: NousResearch#41112 (platform migration tracking), NousResearch#41065 (QQBot plugin)
k176060444-lgtm added a commit to k176060444-lgtm/hermes-agent that referenced this pull request Sep 1, 2026
Round 2 of PR NousResearch#40457 rectification — addresses all four blocking items
from the maintainer review.

1. Live adapter media dispatch (_send_via_adapter / _dispatch_live_media):
   - Text → adapter.send()
   - Image → adapter.send_image_file()
   - Voice  → adapter.send_voice()
   - Video  → adapter.send_video()
   - Other  → adapter.send_document()
   - force_document=True → all via adapter.send_document()
   - Multi-attachment: text sent once, each media sent separately
   - On failure: error returned immediately — no standalone fallback

2. Shared QQ outbound component (gateway/platforms/qqbot/outbound.py):
   - NEW: QQApiClient — token acquisition, auth headers, API requests,
     file upload (ChunkedUploader), text/media message sending
   - Used by standalone sender; structured for future adapter adoption
   - classify_media_type(), resolve_target(), split_for_qq() — single
     source of truth for extension→file_type, prefix→target parsing,
     and text chunking

3. Standalone rewrite:
   - Uses QQApiClient instead of duplicating REST protocol
   - Resource cleanup: try/finally → aclose() on httpx client
   - Guild text supported; guild media explicitly rejected

4. Platform registration:
   - adapter_factory=QQAdapter (no longer lambda cfg: None)
   - check_fn=check_qq_requirements (real dependency check)
   - Registration raises on failure (no bare except: pass)

Test results (--override-ini addopts):
  127 existing + 36 new = 163 passed, 0 regressions
  6 pre-existing TestSendViaAdapterStandaloneFallback failures
    (pytest-asyncio not installed — identical on baseline branch)

Maintainer review: NousResearch#40457 (teknium1)
Related: NousResearch#41112, NousResearch#41065
k176060444-lgtm added a commit to k176060444-lgtm/hermes-agent that referenced this pull request Sep 1, 2026
…ation

Round 3 of PR NousResearch#40457 rectification.

1. QQAdapter now creates and uses QQApiClient for token, REST requests,
   and file uploads — with a transparent fallback to the adapter's own
   logic during startup (before connect() wires up the client).  This
   makes QQApiClient the single shared QQ outbound implementation used
   by both the live adapter and the standalone sender.

2. Added normalize_target() to QQAdapter — strips explicit prefixes
   (c2c:, user:, group:, guild:) before URL construction.  Honors
   explicit prefixes regardless of _chat_type_map.  Raw OpenIDs fall
   back to _guess_chat_type() for inbound compatibility.  Updated all
   outbound methods (send(), _send_media(), _send_chunk()) to use it.

3. Unified media classification: _dispatch_live_media now imports
   classify_media_type from gateway.platforms.qqbot.outbound — live
   and standalone now classify files identically.  No more duplicate
   extension lists.

Deleted duplicate code:
  - _ensure_token HTTP logic (~30 lines)
  - _api_request HTTP logic (~30 lines)
  Both now delegate to QQApiClient, with legacy fallbacks preserved
  for pre-connect() startup paths.

Test results (with pytest-timeout + pytest-asyncio installed):
  330 passed, 0 failures, 0 deselected
    - 161 gateway QQBot tests
    - 133 send_message_tool tests (inc. 6 previously-failing
      TestSendViaAdapterStandaloneFallback)
    - 36 new rectification tests

Related: NousResearch#40457 (teknium1 review), NousResearch#41112, NousResearch#41065
k176060444-lgtm added a commit to k176060444-lgtm/hermes-agent that referenced this pull request Sep 1, 2026
Round 4 of PR NousResearch#40457 rectification.

1. QQApiClient is now the single token/API implementation:
   - asyncio.Lock singleflight with double-check caching
   - public invalidate_token() — adapter no longer touches private fields
   - Read-only access_token / token_expires_at compat properties
   - Adapter _ensure_token / _api_request now ONLY delegate to _api
   - No legacy HTTP fallback (deleted ~70 lines of duplicate protocol)
   - disconnect() clears _api after closing _http_client

2. Structured QQApiError with status_code:
   - 404 detection for raw-OpenID C2C→group fallback
   - 401/403/429/timeout/5xx never trigger fallback
   - resolve_target() returns 3-tuple (type, id, has_prefix)
   - normalize_target() honors has_prefix; raw OpenIDs delegate to
     _guess_chat_type()

3. is_voice parameter in classify_media_type():
   - Passed through from tool-layer _is_voice flag
   - force_document is highest priority
   - Live (_dispatch_live_media) and standalone both use same function

4. 4004 handler calls self._api.invalidate_token()
   - Next reconnect forces fresh token acquisition

5. Tests: 318 passed, 0 failures
   - token singleflight: concurrent ensure_token → 1 POST
   - invalidate clears cache; readonly properties work
   - adapter delegation; no _api raises RuntimeError
   - cleanup clears _api
   - is_voice classification (5 tests)
   - target has_prefix semantics
   - QQApiError.status_code
   - >7 MB sparse file reaches mocked ChunkedUploader
   - guild text dispatch; force_document live+standalone
   - live failure no standalone fallback

Related: NousResearch#40457 (teknium1), NousResearch#41112, NousResearch#41065
k176060444-lgtm added a commit to k176060444-lgtm/hermes-agent that referenced this pull request Sep 1, 2026
…xclusivity

Round 5 of PR NousResearch#40457 rectification.

1. Raw OpenID 404-only fallback (standalone.py):
   - resolve_target() returns has_prefix flag
   - Only raw OpenIDs (has_prefix=False) may fallback C2C→group on 404
   - Explicit c2c:/user:/group:/guild: never fallback
   - 401/403/429/timeout/5xx/non-JSON errors never fallback
   - Text/upload not double-sent: retry happens before final send
   - Fallback lives in platform-owned outbound layer (standalone.py)

2. is_voice has real semantics:
   - classify_media_type(is_voice=True) ONLY gates to MEDIA_TYPE_VOICE
     when the extension is also in _VOICE_EXTS
   - Without [[audio_as_voice]], audio files (.mp3/.ogg/etc.) → MEDIA_TYPE_FILE
   - force_document always highest priority
   - Matches Telegram/Slack is_voice contract from BasePlatformAdapter

3. QQApiClient is single source of token truth:
   - Removed _fallback_token from adapter
   - _ensure_token() purely delegates to _api (no fallback path)
   - Adapter compat properties delegate to _api
   - Updated old TestIdentifyIntents test to mock QQApiClient

4. Strengthened QQApiClient error handling:
   - Non-JSON responses still produce QQApiError(status_code=X)
   - Error text never leaks token/secret (only path)
   - 5xx/timeout treated correctly

Tests: 320 passed, 0 failures
   - is_voice behavioral (7 tests): True→voice, False→file, force_doc
   - 404 fallback (8 tests): raw→group, explicit→no-fallback,
     403/429/5xx/timeout→no-fallback
   - Token singleflight + invalidate
   - Adapter delegation + cleanup
   - Large file → ChunkedUploader
   - Non-JSON error handling
   - force_document + live no-fallback

Related: NousResearch#40457 (teknium1), NousResearch#41112, NousResearch#41065
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/qqbot QQ Bot adapter type/refactor Code restructuring, no behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants