mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-25 16:44:36 +00:00
* feat(channels): add GitHub polling adapter with notification-as-wakeup architecture
Introduce a GitHub channel adapter that monitors notifications and
responds to @mentions on issues/PRs by posting comments. Uses
last_read_at as a per-thread watermark for comment enumeration,
replacing the unreliable latest_comment_url approach.
Foundation changes to ChannelBase:
- sendThreadMessage for thread-targeted delivery (IM adapters unchanged)
- Envelope.metadata appended to prompt after command parsing
- chat_thread session scope (channel:chatId:threadId) prevents
cross-repo session collision
- polling-helpers: testBotMention/stripBotMention (separate detection
from stripping, no whitespace collapsing), cursor persistence,
abortableSleep
GitHub adapter design:
- Notifications as wake-up signals only (unread filtering)
- listComments enumeration with last_read_at watermark
- Bot self-comment filtering, case-insensitive mention regex
- In-memory recentlyProcessed set for mark-read failure dedup
- First-contact: new issue body @bot triggers processing
- Error comment + cursor advance on handleInbound failure
- pollInterval minimum 60s, exponential backoff 2s-30s
* refactor(channels): extract PollingChannelBase from polling-helpers
Replace the loose polling-helpers module with a PollingChannelBase<Cursor>
abstract class that encapsulates the poll loop, cursor persistence (JSON,
atomic write), exponential backoff, and start/stop lifecycle. Subclasses
implement only pollOnce() and createInitialCursor().
- Delete polling-helpers.ts (cursor fns + abortableSleep moved into base)
- Move mention utilities (testBotMention/stripBotMention) to github pkg
- GithubAdapter now extends PollingChannelBase<{ lastProcessedAt }>
* fix(channels): remove Gitea/GitLab mention from sendThreadMessage JSDoc
* fix(channels): match /pulls/N in notification subject URL
GitHub PR notifications use /repos/{owner}/{repo}/pulls/{N} in
subject.url, not /issues/{N}. The regex only matched /issues/,
causing PR notifications to be skipped and marked read.
Also sets threadId to 'pr:N' for PRs (was always 'issue:N').
* test(channels): add PR body first-contact unit test
Verify that PR notifications with @mention in the body (not a comment)
correctly trigger the first-contact path: extractFromSubjectUrl matches
/pulls/N, listComments returns empty, tryFirstContactBody fetches the
PR body and dispatches to handleInbound with threadId 'pr:N'.
* feat(channels): read pollInterval from channel config in PollingChannelBase
Move pollInterval config reading from GithubAdapter to the base class.
The user's configured pollInterval in settings.json is now respected
directly without a minimum enforcement. Defaults to 60000ms when not
configured.
* fix(channels): prepend metadata before prompt text
Agent sees issue/PR context (type, title, URL) before the user's
request, improving comprehension. Metadata is still appended after
slash-command parsing so commands are not affected.
* refactor(channels): route all ChannelBase delivery through sendThreadMessage
Replace all internal sendMessage calls with sendThreadMessage, passing
envelope.threadId (or target.threadId / undefined) so polling adapters
can deliver to the correct thread. IM adapters are unaffected — the
default sendThreadMessage falls through to sendMessage.
* docs(channels): document sendThreadMessage delivery architecture
* fix(channels): address review findings
- Cap recentlyProcessed Set at 10k entries to prevent unbounded growth
- Validate cursor JSON shape (non-null object) in loadCursorFromDisk
- sendThreadMessage falls through to sendMessage when threadId is
undefined instead of silently dropping
- Remove duplicate pollInterval from GithubConfig (now in ChannelConfig)
- Fix chat_thread routing key trailing colon when threadId is undefined
* docs(channels): fix metadata JSDoc — prepended, not appended
* fix(channels): use recentlyProcessed dedup for first-contact body
Replace the fragile createdAt-vs-cursor check in tryFirstContactBody
with the recentlyProcessed set. The cursor advances globally based on
notification updated_at — when a different notification with a later
updated_at is processed first, the cursor can advance past the issue's
created_at, causing the first-contact check to incorrectly skip the
issue body (forget reply bug, found in E2E TC-2b).
* refactor(channels): two-layer dedup for GitHub adapter
Layer 1: global cursor filters notifications by updated_at (sorted
ascending, old first). Layer 2: server-side last_read_at filters
comments by created_at (sorted ascending).
- Delete recentlyProcessed Set (no longer needed)
- Sort notifications by updated_at ascending before processing
- Sort comments by created_at ascending before processing
- Pass latest comment created_at to markThreadAsRead as last_read_at
* fix(channels): address review findings on GitHub adapter
Blockers:
- sessionScope: add defaultSessionScope to ChannelPlugin, apply in
parseChannelConfig so router and adapter agree on 'chat_thread'
- channel-registry.test.ts: add 'github' to expected type list
Should-fix:
- Replace per-thread markThreadAsRead (PATCH) with bulk
markNotificationsAsRead (PUT /notifications + last_read_at).
API errors stop the batch without marking failed notifications
read; handleInbound errors still advance (error comment posted).
- connect() throws on bot identity failure instead of failing open
- metadata appended after promptText (inside sender attribution)
- isSharedSessionTarget includes 'chat_thread' scope
Nits:
- startPollLoop re-entrancy guard
- clean-package-build-artifacts.js includes github
- index.ts re-exports GithubChannel
* fix(channels): use max updated_at of all fetched notifications as last_read_at
Prevents re-fetching the same notifications in the next poll cycle.
The bulk PUT /notifications marks all fetched notifications as read
up to the max updated_at, regardless of per-notification success.
* fix(channels): address review round 2 findings
- #12: loadCursorFromDisk rejects arrays
- #13: pollInterval validates positive finite number
- #19: first-contact gate uses dispatchedMention flag (not newComments.length)
- #25: stripBotMention no longer trims (preserves indentation)
- #27: remove adapter-level requireMention, unify on GroupGate
- #31: add chat_thread SessionRouter routing key tests
- #33: clear metadata on collect-mode synthetic envelope
- #35: fix PollingChannelBase.test import path
- #36: add @octokit/rest to 15-channel-adapters.md dependencies
* docs(channels): document known limitations for GitHub adapter
- First start skips existing unread notifications (cursor = now)
- Requires classic PAT (fine-grained PATs lack notifications API)
- PR review comments not enumerated (issue comments only)
* fix(channels): address review round 3 findings
- #9: buildMetadata derives web URL from baseUrl (GHE support)
- #12: sendThreadMessage throws on invalid threadId format
- #19: mention lookbehind matches cc:@bot and "@bot" patterns
- #23: cursor file name uses sha256 hash to prevent collision
- #26: test verifies cursor persistence to disk
- #31: postErrorComment double-failure logs to stderr
- #45: tests use mkdtempSync isolation instead of real QWEN_HOME
* fix(channels): pass threadId through pairing flow + sendResponseMessage test
- #13+16: onPairingRequired receives envelope.threadId and passes it
to sendThreadMessage, so pairing codes are delivered on threaded
channels (GitHub) instead of throwing
- #6: add test verifying sendResponseMessage resolves threadId from
router.getTarget and passes it to sendThreadMessage
* fix(channels): pass proxy to Octokit for daemon-worker environments
- #44: read this.proxy from ChannelBaseOptions and pass
HttpsProxyAgent to Octokit request.agent, matching the
Telegram adapter pattern
* fix(channels): address review findings — immutable senderId, comment time window, validateCursor, retry wrapper
- senderId uses immutable user.id; allowedUsers resolved to IDs at connect
- Comment filter upper bound: updated_at <= maxUpdatedAt (batch window)
- Per-notification errors use continue (best-effort), not break
- validateCursor() virtual hook for subclass cursor shape validation
- sendThreadMessage/postErrorComment wrapped in githubApi() retry
- webOrigin handles default api.github.com → github.com
- Docs: classic PAT only, markNotificationsAsRead, dedup claims removed
- Tests: threadId priority, metadata consumption, defaultSessionScope,
QWEN_HOME isolation, persistent mock rejection
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(channels): mark notifications read before processing to prevent duplicate replies
Bot's own replies bump notification updated_at past the pre-captured
maxUpdatedAt, so markNotificationsAsRead(maxUpdatedAt) failed to mark
them read — the next poll re-fetched the same comments and replied
again.
Move markNotificationsAsRead + cursor advance before the processing
loop (best-effort delivery). This is safe because bot's own comments
do not flip notifications back to unread. Update docs to reflect the
new poll cycle order and best-effort semantics.
* fix(channels): update sender gate after allowedUser ID resolution and harden tests
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(channels): cursor-based comment window to prevent duplicate replies
PUT /notifications is async (202) with a last_read_at cutoff — the
bot's reply bumps updated_at past the cutoff before the server
processes the mark, so the notification is never marked read and gets
re-fetched on the next poll, causing duplicate replies.
Use the cursor value before advancement as an exclusive lower bound
for the comment enumeration window: (windowSince, maxUpdatedAt].
Comments already eligible in a previous poll are excluded regardless
of whether the mark succeeded. Zero new persistent state.
* fix(channels): cursor-based comment window to prevent duplicate replies
PUT /notifications is async (202) with a last_read_at cutoff — the
bot's reply bumps updated_at past the cutoff before the server
processes the mark, so the notification is never marked read and gets
re-fetched on the next poll, causing duplicate replies.
Use the cursor value before advancement as an exclusive lower bound
for the comment enumeration window, with per-notification last_read_at
as the preferred lower bound when available (server-side per-thread
watermark). Comments already eligible in a previous poll are excluded
regardless of whether the mark succeeded. Zero new persistent state.
* fix(channels): address review findings — null guard, cursor validation, metadata dedup, abortable sleep, docs
- Guard against null notification.subject.url in pollOnce
- Validate lastProcessedAt is a parseable date in validateCursor
- Add metadata: undefined to second collect-mode drain path
- Refactor abortableSleep as protected method on PollingChannelBase
- Fix docs: requireMention is nested under groups.*
- Add tests: chat_thread shared session, dispatchedBodies eviction,
cursor enumeration window, last_read_at in mention tests
* docs(channels): sync docs with implementation — cursor shape, error handling, GitHub adapter tables, first-contact
- Design doc: update Cursor to { lastProcessedAt, dispatchedBodies? }, add
validateCursor date check, abortableSleep protected method, break-on-error
semantics, subject.url null guard
- Developer docs: add GitHub to adapter table and adapter matrix
- User guide: add first-contact step to How It Works, clarify mark-before-process
* fix(channels): address review round 2 — error dedup, abortable retry, backoff reset, window test
- Record dispatchedBody on first-contact handleInbound failure to prevent
duplicate error comments when mark-read async hasn't taken effect
- Use abortableSleep instead of raw setTimeout in githubApi retry so
disconnect() can interrupt rate-limit cooldowns
- Reset consecutiveErrors in startPollLoop so stop/restart cycles don't
inherit stale elevated backoff
- Add test for cursor window client-side lower-bound exclusion filter
* fix(channels): address review round 3 — cursor validation, error dedup, sender gate, bot-self body
- validateCursor: normalize falsy non-array dispatchedBodies (false/0/""/null)
to [] instead of passing them through to .includes() which throws TypeError
- Set dispatchedMention after postErrorComment to prevent first-contact from
posting a duplicate error comment on the same thread
- Only set dispatchedMention when the sender passes the sender gate, so a
disallowed commenter's mention no longer suppresses a valid first-contact
body from an allowed issue author
- Skip bot-authored issue bodies in tryFirstContactBody to prevent
self-response loops under open sender policy
* fix(channels): address review suggestions — test coverage, cursor filename, assertion precision
- Pairing flow: add threadId pass-through regression test
- pollInterval: add table-driven edge cases (0, -1, NaN, Infinity, string)
- Add null-URL notification followed by valid notification batch test
- Fix comment window test to assert paginate call 3 (listComments) not call 2
- Truncate cursor filename encoded prefix to 200 chars (filesystem 255 limit)
- Assert mark-read uses batch maxUpdatedAt, not just { read: true }
- Assert real GitHub plugin declares defaultSessionScope chat_thread
- Add invocationCallOrder assertion for mark-before-process ordering
* fix(channels): address review round 4 — allowedUsers throw on resolve failure, crash table fix, mark-read failure test
* fix(channels): address review round 5 — created_at filter, retry-after NaN guard, retry/sendThreadMessage tests, docs fixes
* fix(channels): address ci-bot review 4778587403 — reconnect idempotency, github type enumerations, retry/webOrigin tests
* chore(channels): align channel-github version to 0.21.0 after upstream merge
* chore(channels): update package-lock.json for channel-github 0.21.0
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: OrbitZore <orbitzore@users.noreply.github.com>
133 lines
3.9 KiB
JSON
133 lines
3.9 KiB
JSON
{
|
|
"name": "@qwen-code/qwen-code",
|
|
"version": "0.21.0",
|
|
"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.21.0"
|
|
},
|
|
"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/audio-capture": "file:../audio-capture",
|
|
"@qwen-code/channel-base": "file:../channels/base",
|
|
"@qwen-code/channel-dingtalk": "file:../channels/dingtalk",
|
|
"@qwen-code/channel-wecom": "file:../channels/wecom",
|
|
"@qwen-code/channel-qqbot": "file:../channels/qqbot",
|
|
"@qwen-code/channel-github": "file:../channels/github",
|
|
"@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/sdk": "file:../sdk-typescript",
|
|
"@qwen-code/web-templates": "file:../web-templates",
|
|
"ansi-regex": "^6.2.2",
|
|
"chokidar": "^4.0.3",
|
|
"command-exists": "^1.2.9",
|
|
"comment-json": "^4.2.5",
|
|
"diff": "^7.0.0",
|
|
"dotenv": "^17.1.0",
|
|
"express": "^5.2.1",
|
|
"fast-deep-equal": "^3.1.3",
|
|
"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",
|
|
"proper-lockfile": "^4.1.2",
|
|
"prompts": "^2.4.2",
|
|
"react": "^19.2.4",
|
|
"read-package-up": "^11.0.0",
|
|
"shell-quote": "^1.9.0",
|
|
"simple-git": "^3.36.0",
|
|
"string-width": "^7.1.0",
|
|
"strip-ansi": "^7.1.0",
|
|
"strip-json-comments": "^3.1.1",
|
|
"tar": "^7.5.19",
|
|
"undici": "^7.28.0",
|
|
"wrap-ansi": "^10.0.0",
|
|
"ws": "^8.18.0",
|
|
"yauzl": "^2.10.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/yauzl": "^2.9.1",
|
|
"@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"
|
|
}
|
|
}
|