mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
5 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8fcc43943d
|
feat(channel): add QQ Bot (QQ机器人) channel adapter (#5202)
* feat(channel): add QQ Bot channel adapter Add @qwen-code/channel-qqbot package implementing QQ Bot WebSocket Gateway connection via the official QQ Bot API. Supports: - WebSocket Gateway (HELLO/IDENTIFY/HEARTBEAT/DISPATCH/RECONNECT) - C2C single chat (C2C_MESSAGE_CREATE) - Group @mention (GROUP_AT_MESSAGE_CREATE) — code path exists, unverified - Streaming output via msg_id + msg_seq multi-block sending - Auto-reconnect with exponential backoff - Sandbox environment toggle TODO (technical debt acknowledged): - Group chat not verified end-to-end - Single-file architecture (should split into gateway/send/auth modules like weixin channel) - No tests (weixin has send.test.ts + media.test.ts) - No typing indicator (onPromptStart/onPromptEnd not yet implemented) - No channel instructions injection in connect() - No structured error types Closes #5201 * feat(qqbot): add QR login, group chat support with typed events - Add QR code login via @tencent-connect/qqbot-connector with credential persistence - Add Intent constants for C2C (1<<12) and GROUP_AT_MESSAGE (1<<25) - Use QQGroupMessageEvent type in handleGroup instead of cast - Remove resolved TODO comments for group chat verification - Add msg_seq to send error log for debugging * fix(qqbot): address PR review — lint errors, token refresh, security - Use bracket notation for Record<string, unknown> to fix TS4111 lint errors - Add chmodSync(credsFile, 0o600) for credential file permissions - Implement token refresh at 80% TTL with expires_in tracking - Fix RECONNECT opcode: use code 4000 + serverRequestedReconnect flag - Fix connect() Promise: reject on close before READY via connectReject - Log empty-token case in sendMessage, drain response body on error - Clear chatTypeMap/replyMsgId/msgSeqMap in disconnect() - Capture msgId at send-time to avoid race on replyMsgId - Switch channel-registry.ts to Promise.allSettled (isolated channel failures) - Add chatId validation (isValidChatId) to prevent SSRF * fix(qqbot): add qqbot to build order, fix ESLint default-case - Add packages/channels/qqbot to scripts/build.js buildOrder (CLI imports @qwen-code/channel-qqbot but it wasn't being built) - Add default case to handleGatewayMessage switch * feat(qqbot): prepend sender name in group messages for shared context When sessionScope is set to 'thread', all group members share one session. Prepending [senderName] helps the agent distinguish who said what in the shared context. * feat(qqbot): cross-server context continuation via SessionRouter persistence - Persist SessionRouter mappings to disk via sessionsPath, surviving daemon restarts - Persist QQ routing state (chatTypeMap, replyMsgId, msgSeqMap) to {name}-state.json - Backup/restore global sessions.json on disconnect/connect to survive start.ts cleanup - fixRestoredSessions() workaround for ACP LoadSessionResponse missing sessionId - READY handler delays resolve() until restoreSessions() completes, preventing race * feat(qqbot): add Session Resume + reconnect retry resilience - Support WS session resume (RESUME opcode 6) on reconnect, falling back to full IDENTIFY when session is invalid - Add reconnectWithRetry() loop: retries gateway fetch up to 5x with exponential backoff, then schedules 60s fallback retry (fixes silent death after GW HTTP 500) - connect() now retries up to 3 times on initial failure - Bump maxReconnectAttempts from 10 to 20 - Refresh token before each reconnect attempt * fix(qqbot): address review feedback from wenshao - fixRestoredSessions: use entry.target directly instead of tt.get(undefined) (fixes first restored session routing to wrong conversation when 2+ sessions) - scheduleTokenRefresh: retry in 60s on token refresh failure, not just log - sendMessage: move saveQQState() after chunk loop, avoid redundant disk I/O - handleGroup: drop message when group_openid is missing instead of falling back to author.id (which would cause 404 on group message send) * fix(qqbot): address 3rd review from doudouOUC (12 issues) - QWEN_HOME: use getGlobalQwenDir() instead of homedir() - name sanitization: prevent path traversal in file paths - fetch timeouts: AbortSignal.timeout(15s) on all 3 fetch calls - TOCTOU: writeFileSync with {mode: 0o600} instead of chmodSync after - msg_seq gaps: only increment seq on send success, break on failure - message dedup: seenMessages Map with 5min TTL cleanup timer - disconnect: set disposed flag + flushQQState sync + clear timers - heartbeat ACK: track lastHeartbeatAck, force close on 2x interval timeout - reconnect exhaustion: FATAL log when max attempts reached post-connect - debounced saveQQState: 500ms debounce, flush on disconnect - handleGroup: skip [senderName] prefix for slash commands, log for audit - disposed guard: connectGateway checks disposed before creating WS * fix(qqbot): robustness round — RESUMED, token expiry, SSRF, disposed, typing stubs - Handle RESUMED event on RESUME success (start heartbeat, restore sessions) - Check token expiry before sendMessage, refresh if expired - Tighten isValidChatId regex (remove . and /) to close path traversal - Reset disposed flag in connect() for reusability - Add onPromptStart/onPromptEnd stubs (QQ Bot has no typing API) - Add robustness comments for splitText surrogate pairs, restoreQQState corruption, and senderId identity fragmentation across contexts * refactor(qqbot): split into modules — api, accounts, login Extract HTTP calls, credential I/O, and QR login into separate files matching the weixin channel's architecture: - api.ts: fetchAccessToken, fetchGatewayUrl, getApiBase, sendQQMessage - accounts.ts: getCredsFilePath, loadCredentials, saveCredentials - login.ts: qrCodeLogin (qrConnect wrapper) QQChannel.ts drops inline fetch/credential/qrConnect logic and imports from the new modules. Net -41 lines in the adapter. * feat(qqbot): markdown message support (msg_type: 2) Detect markdown syntax in AI responses and send as msg_type=2 with markdown.content field instead of plain-text msg_type=0. Detection covers headers, code blocks, bold, italic, strikethrough, inline code, links, and lists via a single regex. * fix(qqbot): defensive patches from complete review - reconnectWithRetry: guard against disposed channel to prevent infinite loop - handleGroup: broaden @mention regex to match both legacy <@!id> and V2 <@openid> - handleGroup: set isReplyToBot=true (every group msg is an @mention) - fixRestoredSessions: document fragile private-field access - saveCredentials: correct TOCTOU claim in comment - hasMarkdownSyntax: document false-positive trade-off * fix(qqbot): guard against empty content in C2C and group handlers - handleC2C: return early when event.content is null/empty (image/sticker msgs) - handleGroup: return early when cleanText is empty after @mention stripping * fix(qqbot): close remaining review gaps — disposed guard, connectReject, token retry, RESUMED restore * fix(qqbot): address wenshao review — RESUME restore removal, disposed guards, timer tracking, logging, heartbeat floor, requiredConfigFields, channel-registry error labels * fix(qqbot): markdown fallback to plain text on rejection * docs(qqbot): clarify markdown permission — Open Platform has no gate, FAQ is a different platform * feat(qqbot): add Ark (msg_type=3) and Media (msg_type=7) message support - types.ts: ArkKV, ArkPayload, FileType, MediaUploadRequest/Response, MediaPayload - api.ts: uploadQQMedia() — file upload for rich media - QQChannel.ts: sendArk(chatId, templateId, kv) + sendMedia(chatId, fileType, url, text?) - C2C/group upload paths separated (file_info not interchangeable) - file_type=4 (文件) blocked for groups per QQ API - Embed (msg_type=4) skipped — QQ频道专用, not available for Bot Open Platform * feat(qqbot): auto-route !ark / !media commands from LLM text via sendMessage LLM outputs text — the channel now parses structured commands inline: !ark(24, #TITLE#=标题, #META_DESC#=描述) !media(image, https://example.com/photo.jpg, caption text) parseArkCommand / parseMediaCommand extract at sendMessage entry; normal text/markdown flow unchanged. * feat(qqbot): inject channel instructions for ark/media commands Sets config.instructions on connect() so the LLM learns about: !ark(template_id, key=val, ...) — 3 default templates (23/24/37) !media(type, url, [caption]) — image/video/voice/file Fixes known debt: 'No channel instructions'. * feat(qqbot): gate ark/media behind config flags (enableArk/enableMedia) Both features default to false — opt-in via settings.json: channels.my-qq.enableArk = true channels.my-qq.enableMedia = true Instructions injected conditionally; command routing gated per-flag. * refactor(qqbot): extract resolveRoute() to eliminate duplication across sendMessage/sendArk/sendMedia disposed check, token refresh, chatId validation, sandbox path selection now in one place. All three methods call resolveRoute() instead of repeating the same 15-line preamble. * chore(qqbot): remove Ark and Media message support Remove !ark() / !media() text parsing, sendArk/sendMedia methods, uploadQQMedia, and all related types. The text-parsing approach was too fragile against LLM output formatting. Only text/markdown messaging remains. * fix(qqbot): robustness patches for review findings - Add { mode: 0o600 } to all writeFileSync calls (state/session files) - Guard against stale WebSocket close event nuking new connection - Add isReconnecting guard to prevent parallel reconnectWithRetry chains - Reset isReconnecting flag in READY, RESUMED, and exhaustion paths * docs(channel): add QQ Bot user documentation Add user-facing documentation for the QQ Bot channel adapter: - New docs/users/features/channels/qqbot.md covering setup, configuration, QR code login, group chat, Markdown support, token management, connection resilience, and troubleshooting - Update docs/users/features/channels/_meta.ts to include QQ Bot in nav - Update docs/users/features/channels/overview.md to reference QQ Bot across the intro, quick start, type options, slash commands, and the media platform differences table * docs(qqbot): fix prerequisites — QR login needs no developer account QR code login via qrConnect() does not require a developer account or manual app registration. First qwen channel start is all you need. * docs(qqbot): emphasize QR login, keep developer portal as secondary path Both paths work (config → persisted file → QR scan), confirmed against fetchToken() code. Reposition QR code login as the primary setup flow, remove redundant tips/troubleshooting entries. * docs(qqbot): remove Images and Files section — not supported in channel code handleC2C/handleGroup both skip messages with no text content. No media download or upload logic exists in this channel adapter. * test(qqbot): add unit tests for send utilities Add vitest test suite for QQ Bot channel following the weixin channel testing patterns. Extract isValidChatId, hasMarkdownSyntax, and splitText as exported module-level functions to enable direct testing. - 27 tests covering: chatId SSRF validation, Markdown syntax detection, and text chunking for QQ's 2000-char message limit - Add vitest.config.ts and test script to qqbot package - Register qqbot in root vitest workspace projects Refs: #5202 * test(qqbot): add sendMessage flow tests with mocked API Follow the weixin sendImage test pattern: mock sendQQMessage and channel-base dependencies to test sendMessage end-to-end. - C2C/group routing verification - Markdown msg_type=2 vs plain text msg_type=0 - Markdown rejection fallback to plain text - Disposed guard and error-stop behavior - msg_id + msg_seq tracking for multi-chunk streaming 9 new tests, 36 total (all passing) * test(qqbot): fix review issues — add missing edge cases Self-review fixes: - Fix misleading test name: 'returns early when chatId not in chatTypeMap' → 'defaults to C2C path for unknown chatId' (code doesn't return early) - Add SSRF validation test: sendMessage rejects '../traversal' chatId - Add network error test: thrown sendQQMessage caught by try/catch - Add token expiration test: expired token + failed refresh → early return - Hoist mockFetchAccessToken and set default resolved value in beforeEach to prevent silent undefined-access failures in accidental token-refresh paths 39 tests, all passing * test(qqbot): add api and accounts unit tests Add api.test.ts (13 tests) and accounts.test.ts (8 tests) following weixin channel vitest patterns: vi.hoisted() mocks, vi.mock() module replacement, and dynamic import() after mock setup. api.test.ts covers getApiBase, sendQQMessage, fetchAccessToken, and fetchGatewayUrl — including HTTP errors, missing fields, and request body format. accounts.test.ts covers getCredsFilePath, loadCredentials (missing file, corrupt JSON, missing fields, valid data), and saveCredentials (dir creation + 0o600 permissions). All 60 tests pass (39 existing + 21 new). tsc --build and eslint clean. * chore(qqbot): suppress CodeQL ReDoS false positives Add codeql[js/polynomial-redos] suppression comments for two regexes flagged by CodeQL: - hasMarkdownSyntax(): input is LLM-generated reply text, never attacker-controlled in Qwen Code Channel context. - handleGroup(): <@...> prefix is injected by QQ servers; openid is assigned by QQ, not attacker-chosen. Both paths have no practical exploit vector — an adversary would need to either control an LLM's output or register a malicious openid with QQ, neither of which is achievable. * fix(qqbot): allow QR-code-only login and guard qrConnect return - requiredConfigFields: [] — fetchToken() already resolves credentials from config → persisted file → QR fallback chain. Blocking at config validation prevented QR-code-only users from starting the channel. - qrCodeLogin(): add bounds check for empty qrConnect() return value. If the external library returns an empty array, throw descriptive error instead of crashing with TypeError on creds.appId. * chore(qqbot): add comments for requiredConfigFields and qrConnect guard - index.ts: explain why requiredConfigFields is empty — fetchToken() already resolves credentials via config → file → QR fallback chain. Requiring appID/appSecret at config level would block QR-only users from reaching the fallback through the built-in channel path. - login.ts: clarify qrConnect() guard is a defensive robustness patch, not a response to an observed failure. Verified by removing appID from config and running qwen channel start — QR login triggers correctly and returns valid credentials. * fix(qqbot): replace quadratic regexes with linear patterns, remove failed suppress comments * fix(qqbot): split hasMarkdownSyntax into individual tests to pass CodeQL * fix(qqbot): replace markdown link regex with indexOf to eliminate CodeQL ReDoS |
||
|
|
d84675e86f |
test(channels): add comprehensive test suites for channel adapters
- Add ChannelBase, GroupGate, SenderGate, SessionRouter tests - Add DingTalk markdown utility tests - Add Weixin media and send helper tests - Add CLI channel config-utils and pidfile tests - Configure vitest for all channel packages - Exclude test files from TypeScript build Tests cover attachment handling, block streaming, gating policies, session routing, markdown conversion, config parsing, and service management. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
e1ffaec499 | feat: create draft framework for cli & sdk | ||
|
|
dbdb4db4f0 | fix: lockfile and configs | ||
|
|
3f535195b3 | test: add project settings |