mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
* 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
126 lines
3.6 KiB
JSON
126 lines
3.6 KiB
JSON
{
|
|
"name": "@qwen-code/qwen-code",
|
|
"version": "0.18.3",
|
|
"description": "Qwen Code",
|
|
"repository": {
|
|
"type": "git",
|
|
"url": "git+https://github.com/QwenLM/qwen-code.git"
|
|
},
|
|
"type": "module",
|
|
"main": "dist/index.js",
|
|
"types": "dist/index.d.ts",
|
|
"bin": {
|
|
"qwen": "dist/index.js"
|
|
},
|
|
"exports": {
|
|
".": {
|
|
"types": "./dist/index.d.ts",
|
|
"import": "./dist/index.js"
|
|
},
|
|
"./export": {
|
|
"types": "./dist/src/export/index.d.ts",
|
|
"import": "./dist/src/export/index.js"
|
|
}
|
|
},
|
|
"scripts": {
|
|
"build": "node ../../scripts/build_package.js",
|
|
"start": "node dist/index.js",
|
|
"debug": "node --inspect-brk dist/index.js",
|
|
"lint": "eslint . --ext .ts,.tsx",
|
|
"format": "prettier --write .",
|
|
"test": "vitest run",
|
|
"test:ci": "vitest run",
|
|
"typecheck": "tsc --noEmit",
|
|
"check-i18n": "tsx ../../scripts/check-i18n.ts"
|
|
},
|
|
"files": [
|
|
"dist"
|
|
],
|
|
"config": {
|
|
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.18.3"
|
|
},
|
|
"dependencies": {
|
|
"@agentclientprotocol/sdk": "^0.14.1",
|
|
"@google/genai": "2.6.0",
|
|
"@iarna/toml": "^2.2.5",
|
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
"@qwen-code/acp-bridge": "file:../acp-bridge",
|
|
"@qwen-code/channel-base": "file:../channels/base",
|
|
"@qwen-code/channel-dingtalk": "file:../channels/dingtalk",
|
|
"@qwen-code/channel-qqbot": "file:../channels/qqbot",
|
|
"@qwen-code/channel-feishu": "file:../channels/feishu",
|
|
"@qwen-code/channel-telegram": "file:../channels/telegram",
|
|
"@qwen-code/channel-weixin": "file:../channels/weixin",
|
|
"@qwen-code/qwen-code-core": "file:../core",
|
|
"@qwen-code/web-templates": "file:../web-templates",
|
|
"@types/update-notifier": "^6.0.8",
|
|
"ansi-regex": "^6.2.2",
|
|
"command-exists": "^1.2.9",
|
|
"comment-json": "^4.2.5",
|
|
"diff": "^7.0.0",
|
|
"dotenv": "^17.1.0",
|
|
"express": "^5.2.1",
|
|
"fzf": "^0.5.2",
|
|
"glob": "^10.5.0",
|
|
"highlight.js": "^11.11.1",
|
|
"ink": "^7.0.3",
|
|
"ink-gradient": "^3.0.0",
|
|
"ink-link": "^4.1.0",
|
|
"ink-spinner": "^5.0.0",
|
|
"lowlight": "^3.3.0",
|
|
"p-limit": "^7.3.0",
|
|
"prompts": "^2.4.2",
|
|
"react": "^19.2.4",
|
|
"read-package-up": "^11.0.0",
|
|
"shell-quote": "^1.8.3",
|
|
"simple-git": "^3.28.0",
|
|
"string-width": "^7.1.0",
|
|
"strip-ansi": "^7.1.0",
|
|
"strip-json-comments": "^3.1.1",
|
|
"tar": "^7.5.2",
|
|
"undici": "^6.22.0",
|
|
"update-notifier": "^7.3.1",
|
|
"wrap-ansi": "^10.0.0",
|
|
"ws": "^8.18.0",
|
|
"yargs": "^17.7.2",
|
|
"zod": "^3.23.8"
|
|
},
|
|
"devDependencies": {
|
|
"@babel/runtime": "^7.27.6",
|
|
"@testing-library/react": "^16.3.0",
|
|
"@types/archiver": "^6.0.3",
|
|
"@types/command-exists": "^1.2.3",
|
|
"@types/diff": "^7.0.2",
|
|
"@types/dotenv": "^6.1.1",
|
|
"@types/express": "^5.0.3",
|
|
"@types/node": "^22.0.0",
|
|
"@types/prompts": "^2.4.9",
|
|
"@types/ws": "^8.5.0",
|
|
"@types/react": "^19.1.8",
|
|
"@types/react-dom": "^19.1.6",
|
|
"@types/semver": "^7.7.0",
|
|
"@types/shell-quote": "^1.7.5",
|
|
"@types/supertest": "^6.0.3",
|
|
"@types/yargs": "^17.0.32",
|
|
"archiver": "^7.0.1",
|
|
"ink-testing-library": "^4.0.0",
|
|
"jsdom": "^26.1.0",
|
|
"pretty-format": "^30.0.2",
|
|
"react-dom": "^19.1.0",
|
|
"supertest": "^7.2.2",
|
|
"typescript": "^5.3.3",
|
|
"vitest": "^3.1.1"
|
|
},
|
|
"optionalDependencies": {
|
|
"@teddyzhu/clipboard": "^0.0.5",
|
|
"@teddyzhu/clipboard-darwin-arm64": "0.0.5",
|
|
"@teddyzhu/clipboard-darwin-x64": "0.0.5",
|
|
"@teddyzhu/clipboard-linux-arm64-gnu": "0.0.5",
|
|
"@teddyzhu/clipboard-linux-x64-gnu": "0.0.5",
|
|
"@teddyzhu/clipboard-win32-arm64-msvc": "0.0.5",
|
|
"@teddyzhu/clipboard-win32-x64-msvc": "0.0.5"
|
|
},
|
|
"engines": {
|
|
"node": ">=22"
|
|
}
|
|
}
|