Commit graph

48 commits

Author SHA1 Message Date
Nothing Chan
a771e4449e
fix(channels): reject unusable GitHub self-allowlists (#8055) 2026-07-29 15:48:56 +00:00
Mark Xian
c19d321d1f
feat(github-channel): filter notification reasons (#8031) 2026-07-29 15:34:16 +00:00
OrbitZore
ec9c36ef82
feat(channels): add GitLab polling channel adapter (#7862)
* feat(channels): add GitLab polling channel adapter

Poll GitLab todos via @gitbeaker/rest, dispatch notes through the
existing PollingChannelBase pipeline. Key design points:

- action_prompt_template config drives event filtering and metadata
  rendering (unconfigured actions are skipped)
- Per-repo cursor (repo[chatId].last_read) as notes window lower bound,
  global lastProcessedAt for todo-level dedup
- mark_done after successful processing; failure skips mark_done for
  retry on next poll
- Mention gating delegated to base GroupGate (adapter only sets
  isMentioned flag)
- First-contact body fallback for todos with no notes (e.g. mention in
  issue description)

* fix(channels/gitlab): persist cursor after each successful todo

Call saveCursor() immediately after advancing lastProcessedAt so that
progress is durable even if the process crashes mid-poll. Also removes
the local watermark variable in favor of direct assignment.

* fix(channels/gitlab): persist cursor on every advancement including skips

* fix(channels/gitlab): address review critical issues

- Remove non-functional proxyAgent (gitbeaker doesn't support it)
- Construct repo_url from host + path (API doesn't return web_url)
- Handle directly_addressed action (falls back to mentioned template)
- First-contact fetches target description instead of using todo.body
- Move todo.project dereference inside try block
- Filter confidential notes
- Update channel-registry.test.ts for gitlab entry

* fix(channels/gitlab): address review suggestions

- Warn on connect if action_prompt_template is not configured
- Guard todo.target.iid before use
- Skip paths now mark_done (best-effort) to clean GitLab UI
- Remove postErrorComment (avoids duplicate comments on retry)
- Fetch only first page of notes (desc, maxPages:1, perPage:100)
  instead of paginating entire note history
- Extract fetchRecentNotes for single-page windowed enumeration

* refactor(channels/gitlab): simplify to todo.body dispatch, add description mention support

- Remove notes API fetching; dispatch todo.body directly
- Detect description mentions via target_url anchor (#note_ absence)
- Always fetch target description for %description% metadata
- Remove per-repo cursor; dedup via cursor + mark_done only
- Cursor advances regardless of success/failure (no retry)
- Use zod for cursor validation
- Rename template vars to GitLab terminology:
  %project% %project_url% %target_type% %iid% %title% %description% %todo_id%
- Support %% escape for literal percent

* docs(channels): add GitLab adapter documentation

- New user guide: docs/users/features/channels/gitlab.md
- Update _meta.ts navigation
- Update developer adapter matrix and SDK list

* fix(channels/gitlab): use correct Issues.show(issueIid, { projectId }) signature

* chore: regenerate NOTICES.txt for new gitlab channel dependencies

* fix(channels/gitlab): address review suggestions

- Add todo.project null guard (item 2)
- Single-pass regex for %% escape + %var% substitution (item 4)
- sendThreadMessage throws directly on undefined threadId (item 5)
- Dedup fetchDescription with per-poll cache (item 6)
- Remove per-todo saveCursor; base class saves after pollOnce (item 7)
- Add undefined threadId test (item 8)
- Expand confidential notes limitation in docs (item 3)

* test(channels/gitlab): add mention tests, directly_addressed coverage, skip assertions, temp cleanup

- New mention.test.ts: 14 cases for testBotMention/stripBotMention/escapeRegex
- Add directly_addressed fallback test
- Skip tests now assert TodoLists.done + cursor advancement
- afterEach cleans up mkdtempSync temp dirs

* fix(channels/gitlab): address review round 4

- Non-mention actions (assigned, etc.) set forceMentioned=true to bypass GroupGate
- Merge dead note-filter tests into single 'skips todo authored by bot'
- Log fetchDescription errors to stderr instead of silent swallow
- Post error comment on issue/MR when handleInbound fails (best-effort)

* fix(channels/gitlab): always force isMentioned=true, remove regex re-derivation

The action_prompt_template config is already the event filter, and
GitLab has already decided the mention when creating the todo.
Re-deriving isMentioned via regex on todo.body causes permanent
message loss when the regex misses (description mention + fetch
failure, group mentions). Always set forceMentioned=true so
GroupGate never drops a todo that passed the template filter.

* fix(channels/gitlab): propagate fetchDescription errors for description mentions

For note mentions, description is metadata-only — fetch failure is
logged and swallowed. For description mentions, description IS the
message — fetch failure now propagates to the outer catch, which
posts the ⚠️ error comment so the user knows to re-mention.

* perf(channels/gitlab): clean up stale todos, skip unnecessary fetchDescription

- Mark stale todos (updated_at <= cursor) as done on each poll to
  prevent perpetual re-fetching of pre-existing pending todos
- Skip fetchDescription for note mentions when template does not
  contain %description%, saving one API call per todo
- Update docs: stale todo cleanup, error comment on failure

* docs(channels/gitlab): clarify requireMention is bypassed, template is the real filter

* Apply suggestions from code review

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(channels/gitlab): use todo ID cursor instead of timestamp to eliminate equal-timestamp loss

Timestamp-based cursors (second granularity) could silently destroy
todos sharing the same updated_at as the cursor boundary. Switch to
monotonically increasing todo IDs which are unique and collision-free.

Add initialized flag to preserve first-start drain semantics: pre-existing
pending todos are marked done without dispatch on the first poll cycle.

* fix(channels/gitlab): harden first-poll drain, add ordering tests, fix lockfile

- Replace Math.max(...spread) with reduce to avoid RangeError on large
  backlogs (~100k+ todos). Move initialized=true after the drain work so
  any throw retries the drain instead of falling through to dispatch.
- Add unit tests: identical-timestamp delivery and id-order-when-updated_at-disagrees
  (kills M2 sort mutant).
- Align lockfile: file:../base → ^0.21.0 for channel-base dep.

* fix(channels/gitlab): include dot in mention lookahead for GitLab usernames

GitLab usernames may contain dots (e.g. bot.name). The lookahead
character class inherited from GitHub omitted '.', causing @bot.name
to match as @bot. Add '.' to the negated class.

* docs(channels/gitlab): align docs with ID cursor and drain semantics

- Add first-poll drain as step 2 in How It Works
- Clarify GroupGate always passes (isMentioned forced true)
- Document initialized flag in Known Limitations

* Apply suggestions from code review

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(channels/gitlab): align package version and channel-base dependency to 0.21.1

Bump version from 0.21.0 to 0.21.1 to match other channel packages after
upstream merge. Pin @qwen-code/channel-base to exact 0.21.1 instead of
^0.21.0, matching the convention used by other published channels.

* fix(channels/gitlab): regenerate lockfile to match package.json versions

Manually add only gitlab-related lockfile entries (workspace, @gitbeaker
packages, transitive deps, channel-gitlab link) without unrelated npm
normalization churn.

* test(channels/gitlab): add regression tests for first-poll drain hardening

Two tests that kill the M1 (Math.max spread RangeError) and M2 (flag
ordering) mutants which survived the original 46-test suite:

- 150k todo drain verifies reduce() handles large backlogs without
  RangeError and without dispatching
- Drain throw verifies initialized stays false so the next poll retries
  the drain instead of falling through to dispatch

Test file duration: ~40ms → ~170ms.

* docs(channels/gitlab): clarify groupPolicy must be "open" and add runtime warning

The default groupPolicy "disabled" silently drops all mentions — todos are
marked done and cursor advances, but no dispatch occurs. Fix misleading docs
that said "GroupGate always passes" (only true at groupPolicy: "open") and
add a connect()-time warning when groupPolicy is not "open".

* fix(channels/gitlab): correct xcase integrity hash in lockfile

The manually added xcase entry had a typo in the sha512 hash (ys → ks),
causing npm ci EINTEGRITY failures in CI.

* fix(channels/gitlab): correct requester-utils integrity hash in lockfile

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels/gitlab): allow groupPolicy "allowlist" in warning and docs

The groupPolicy warning and docs incorrectly stated that groupPolicy
must be "open". In reality "allowlist" with the project listed also
works because isMentioned is forced true and GroupGate only requires
the group to be listed. Also fix the inaccurate "no error is logged"
claim — ChannelBase logs preflight rejected reason=group_disabled.

Fixes R5-🟡3 from PR #7862 review.

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-29 14:28:31 +00:00
易良
dc2f61d910
feat(channels): dispatch GitHub notifications by reason (#7826)
* feat(channels): dispatch GitHub notifications by reason

Route each GitHub notification by notification.reason into one of five
lanes, instead of dispatching every new comment regardless of trigger:

- mention: only dispatch comments that actually @ the bot (noise reduction)
- review_requested (PR): fetch PR meta via pulls.get and dispatch a
  review-specific prompt, even with no new comments
- assign: fetch issue meta and dispatch a triage-specific prompt
- author/comment: aggregate the window's new comments into one check-and-
  respond prompt
- other reasons: generic fallback (current behavior)

Add cursor dedup via dispatchedComments (by comment node_id) and
dispatchedNotifications (by notification id), surviving a
markNotificationsAsRead failure that leaves the cursor un-advanced.

Closes #7807

* fix(channels): mark review_requested/assign envelopes as mentioned

GroupGate defaults to requireMention: true, which silently drops
isMentioned:false envelopes as 'mention_required'. The review_requested
and assign lanes are explicit directed triggers — the bot was asked to
review or assigned — equivalent to a mention, so set isMentioned: true
so they pass the gate instead of being inert on the documented default
config.

Addresses review Critical on #7826.

* fix(channels): resolve github routing review comments

* fix(channels): dedupe github meta lane comments

* fix(channels): conditional assign framing for PR threads

The assign route already detected PR threads to use pulls.get, but the
trigger framing text always read 'assigned to this issue' even for PRs.
Make it conditional so PR assignments read 'assigned to this pull request'.

* fix(channels): dedup meta lane dispatch inputs

* fix(channels): simplify GitHub reason dispatch

* fix(channels): respect mention gate for github aggregate lane

* fix(channels): truncate aggregate comment bodies by code points

Match the code-point-aware truncation already used for meta-lane bodies
so a supplementary-plane emoji at the MAX_COMMENT_CHARS boundary is not
split into a lone surrogate.

* fix(channels): harden GitHub dispatch failures, event window, and framing (#7826)

- Classify deleted/transferred subjects (404/410) as terminal so a single
  dead notification is logged and skipped instead of wedging the batch's
  mark-read and cursor advance every poll.
- Widen the review_requested/assign event search to the newest ~100 events
  by merging the preceding page when the last page is partial, instead of
  inspecting only the last page (which can hold a single event).
- Move the aggregate lane's untrusted-data warning to the head of the prompt
  text so it precedes the comment text it describes (metadata is appended
  after text by ChannelBase).
- Add regression tests: permanent-failure two-poll advance, terminal 404
  no-retry, multi-page event search, prompt caps, and the no-actor guard.

* fix(channels): drop lastReadAt filter in findMetaTrigger, add review coverage (#7826)

* fix(github): keep aggregate and meta windows bounded

* fix(channels): apply windowSince lower bound in findMetaTrigger (#7826)

* fix(channels): bound retry wedge, compute aggregate isMentioned, fix pairing pre-filter (#7826)

* fix(github): record dispatch before handler

* fix(github): persist skipped notifications

* fix(github): close dispatch retry loss cases

* test(github): cover cursor trim and meta floor validation

* fix(github): simplify notification reason dispatch

* fix(github): preserve batched dispatch comments

* fix(github): restore direct event dedup

* fix(github): preserve directed mention context

* fix(github): keep review fixes scoped

* fix(github): preserve delayed direct triggers

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
2026-07-28 14:05:21 +00:00
Shaojin Wen
4958120c21
docs(channels): Document loops and proactive delivery (#7628)
* docs(channels): document loops and proactive delivery

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(channels): clarify standalone vs daemon loop storage paths (#7628)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-26 07:32:18 +00:00
OrbitZore
4895726600
fix(channels): use username as senderId in GitHub adapter to fix allowlist gate (#7727)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
* fix(channels): use username as senderId in GitHub adapter to fix allowlist gate

isAuthorizedForSharedSessionTarget compares config.allowedUsers (logins)
against envelope.senderId — but senderId was a numeric ID resolved via
getByUsername, so every allowlisted user was rejected from /who, /clear,
/status, /loop, and channel memory commands.

Fix by using user.login as senderId throughout, assuming GitHub users
don't change usernames. This also removes the getByUsername resolution
step that made connect() non-idempotent on daemon reconnect.

- Remove botUserId field; bot self-filter uses botUsername
- Remove allowedUsers login-to-ID resolution in connect()
- Pass config.allowedUsers logins directly to gate
- senderId in envelopes uses user.login

* fix(channels): normalize allowlist/senderId to lowercase for case-insensitive matching

GitHub logins are case-insensitive but Set.has/Array.includes are not.
Without normalization, allowedUsers: ['Alice'] silently rejects a
commenter whose canonical login is 'alice' — a regression from the old
getByUsername round-trip which normalized casing implicitly.

- Normalize config.allowedUsers and gate to lowercase in connect()
- Lowercase senderId at both envelope assignment sites
- Remove dead != null guard in bot self-comment filter
- Add case-insensitive gate test and connect idempotency test
- Add senderId/allowedUsers comparability guard to dispatch test
- Document username-based allowlist rename risk in security section
2026-07-26 00:21:22 +00:00
OrbitZore
62e009a952
feat(channels): GitHub polling adapter with notification-as-wakeup architecture (#7632)
* 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>
2026-07-25 09:31:50 +00:00
Nothing Chan
357619a5fc
fix(channels): scope pairing and allowlist state by workspace (#7065)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
* fix(channels): scope pairing and allowlist state by workspace

PairingStore keyed its on-disk files by channel name alone, under the
global ~/.qwen/channels/ directory. Two workspace-scoped channel
configurations using the same channel name therefore shared pairing
requests and allowlist entries: a sender approved for workspace A was
implicitly approved for workspace B — an authorization-boundary
violation in multi-workspace daemon deployments.

PairingStore now takes the channel's workspace cwd and stores state
under channels/<basename>-<sha256[:12]>/, ChannelBase passes
config.cwd, and the pairing CLI commands gain a --cwd option
(defaulting to the current directory) so list/approve address the same
workspace-scoped store the channel worker uses.

Migration is a conservative one-time grandfather: on first scoped use,
existing legacy global files are COPIED into the scope (so
already-approved senders stay approved and other workspaces can
grandfather the same baseline later), after which the stores diverge —
no ongoing cross-workspace sharing, and legacy content can never
overwrite scoped state.

Fixes #7017

* fix(channels): canonicalize scope identity and gate migration per directory

Address the review findings on #7065:

1. Scope identity now follows the repo's workspace-canonicalization
   contract: getWorkspaceScopeDirName realpaths the resolved path (with
   the same ENOENT fallback as acp-bridge's canonicalizeWorkspace, which
   channel-base mirrors locally to stay dependency-free). Symlinked and
   platform-case-variant spellings of one directory — macOS /tmp/ws vs
   /private/tmp/ws — now address the same store from a daemon worker
   and from the CLI's --cwd.

2. Legacy grandfathering is gated at the scope-directory level instead
   of per file: once the scoped directory exists, legacy files are
   never consulted again. A per-file gate let a legacy allowlist
   silently re-approve senders an operator had revoked by deleting the
   scoped allowlist file, and let an in-use scope absorb a legacy file
   that appeared later. The README now spells out that revocation means
   removing entries, not deleting files.

3. The empty `pairing list` output names the workspace scope and points
   at --cwd, mirroring the approve error, since a scope mismatch
   surfaces there first.

Four new regression tests (symlink collapse, ENOENT fallback, no
resurrection after revoke, no late-legacy absorption) fail on the
previous commit and pass here.

Refs #7017

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(channels): state the broad realpath fallback is intentional; make the ENOENT scope assertion meaningful

Two round-2 review notes on #7065:

- canonicalizeWorkspacePath's docblock claimed to match acp-bridge's
  ENOENT-only fallback while the catch swallows every realpath error.
  Keep the broad catch — pairing storage is best-effort and a transient
  FS error must not stop the channel from starting — and document that
  divergence explicitly instead.

- The ENOENT-fallback test's second assertion compared a scope name to
  itself. It now compares against the scope computed from the resolved
  spelling, pinning that the realpath step degrades to a no-op for
  nonexistent paths.

Refs #7017

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(channels): always close the migration gate, normalize nonexistent-path scopes, copy atomically

Address the automated round-2 inline findings on #7065:

- The migration gate is now closed on the very first construction even
  when no legacy files existed: the scope directory itself is the
  "migration decided" marker. Previously a workspace that first ran on
  new code before any legacy state existed left the gate open, and a
  legacy allowlist written later by an older version still running
  concurrently would have been absorbed.

- resolvePath now runs every input through path.resolve, so
  trailing-separator and dot-dot spellings of a path that does not
  exist on disk (where the realpath step cannot help) canonicalize to
  the same scope instead of three different ones.

- Legacy files are copied via temp file + atomic rename, so a crash
  mid-copy cannot leave a truncated scoped file behind the now-closed
  gate, and a concurrent first construction cannot observe a
  half-written allowlist.

Adds three regression tests (late-legacy not absorbed after empty
first startup, nonexistent-path spelling collapse, unreadable legacy
file keeps the constructor best-effort); the first two fail on the
previous commit.

Refs #7017

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(channels): encode channel names in scoped paths, gate migration per channel, wire tests into CI

Three independently reproduced problems in the workspace-scoping change,
found in external review of d8c155ab3:

- Channel names come from unrestricted config keys and were joined into
  the scoped path verbatim, so a name like `../support` climbed out of
  the scope directory and landed every workspace on one shared file at
  the channels root — silently undoing the isolation this PR exists to
  establish. File names now URI-encode the channel name (mirroring
  GroupHistoryStore), common names encode to themselves, and the legacy
  source path is containment-checked as defense in depth.

- The directory-level migration gate let only the FIRST channel of a
  workspace migrate: one process starts several channels in turn, and
  once the first construction created the scope directory, every other
  channel's legacy state was skipped forever. The gate is now a
  per-channel `<channel>.migrated` sentinel inside the scope directory,
  written even when there was nothing to copy.

- A single unreadable legacy file aborted the whole migration loop and
  the gate still closed, so the other (valid) file was never migrated
  and never retried. Files are now copied independently, best-effort,
  via uniquely-named temp files + atomic rename, and scoped files are
  never overwritten.

Also adds the missing test/test:ci scripts to channels/base (matching
its sibling packages), so the package's 784 tests actually run in CI's
`npm run test:ci --workspaces --if-present` sweep.

Four new regression tests (traversal-name isolation, multi-channel
migration, late-channel migration, unreadable-file independence) all
fail on d8c155ab3 and pass here.

Refs #7017

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(channels): read legacy files under the raw name, retry partial migrations, log failures

Round-3 review findings on #7065:

- Legacy sources are read under the RAW channel name again: pre-scoping
  code wrote them unencoded, so looking them up under the encoded name
  made any channel whose name changes under encoding (e.g. "my
  channel") skip its legacy state and permanently lose approved
  senders behind the sentinel. Encoded names remain in use for the
  scoped destinations; the containment check keeps traversal-style raw
  names from reading outside the channels root.

- The sentinel is only written when every present legacy file was
  copied (or already existed). A partial failure (ENOSPC, transient
  I/O) previously closed the gate with incomplete state; now the next
  construction retries the failed file, and per-file stderr warnings
  are emitted so operators can see why senders are missing instead of
  instrumenting the constructor.

- The symlink test cleans up with unlinkSync — rmSync throws EISDIR
  for a symlink to a directory on macOS.

- The pairing CLI gains tests covering --cwd scoping end to end (list
  isolation, empty-scope hint, approve scoping, cross-workspace code
  rejection), plus an explicit return after the mocked-in-tests
  process.exit(1).

The raw-name and partial-retry regression tests fail on 954e76af4.

Refs #7017

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:33:26 +00:00
BaboBen
a9a6ca4ac6
feat(channels): observe group names from inbound messages (#7155)
* feat(channels): expose observed workspace contacts

* fix(channels): address observed contacts review feedback

* docs(channels): design observed group names

* docs(channels): plan observed group names

* feat(channels): observe inbound group names

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-18 09:26:12 +00:00
BaboBen
02cb3a6380
feat(channels): expose workspace-scoped observed contacts (#7109)
* feat(channels): expose observed workspace contacts

* fix(channels): address observed contacts review feedback

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-18 07:35:45 +00:00
qqqys
7705f08305
feat(channels): save explicit multi-fact memory safely (#7092)
* feat(core): guard channel memory secrets

* feat(channels): classify multiple memory facts

* feat(channels): save multiple memory facts

* fix(channels): snapshot classifier memory fields

* fix(channels): snapshot classifier memory elements

* docs(channels): explain multi-fact memory

* docs(channels): clarify multi-fact memory example

* fix(channels): harden memory input snapshots

* fix(channels): log memory intent validation failures
2026-07-17 15:24:11 +00:00
qqqys
b826420d4d
feat(channels): confirm natural memory mutations (#7066)
* feat(channels): add memory mutation confirmations

* feat(channels): confirm natural memory mutations

* docs(channels): explain memory mutation confirmations

* fix(channels): harden pending memory mutations

* fix(channels): activate memory proposals after delivery
2026-07-17 15:06:44 +00:00
qqqys
859095bc98
feat(channels): support natural memory references (#6952)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
* feat(channels): plan targeted memory intents

* fix(channels): harden targeted memory planner

* fix(channels): bound serialized memory manifest

* feat(channels): resolve natural memory references

* fix(channels): harden memory intent routing

* docs(channels): explain natural memory references

* fix(channels): type resolved memory intent dispatch

* fix(channels): route Chinese preference filters
2026-07-15 17:46:23 +00:00
BaboBen
f5bdba724e
fix(wecom): prevent requireMention from disabling group chat (#6948)
* fix(wecom): trust group callback mention scope

Fixes #6939

* docs(wecom): clarify mention-scoped behavior
2026-07-15 15:38:21 +00:00
BaboBen
4b802ca5f9
feat(channels): support DingTalk webhook delivery to direct messages (#6891)
* docs(channels): design DingTalk webhook DM delivery

* docs(channels): translate DingTalk webhook DM design

* feat(channels): support DingTalk webhook direct messages

* fix(channels): isolate DingTalk webhook DM targets

* fix(channels): handle DingTalk direct delivery failures

* fix(channels): reject malformed DingTalk responses
2026-07-15 03:07:30 +00:00
qqqys
fb0239eab4
feat(channels): add structured channel memory management (#6860)
* feat(core): add structured channel memory document

* fix(core): preserve legacy channel memory whitespace

* feat(core): structure channel memory storage

* fix(core): close channel memory races

* test(core): cover channel memory read failures

* feat(channels): parse channel memory item intents

* test(channels): cover memory intent precedence

* feat(channels): manage structured channel memory

* fix(channels): address structured memory review

* feat(cli): wire structured channel memory

* docs(channels): document structured channel memory

* fix(core): harden channel memory persistence

* fix(core): preserve channel memory uniqueness

* fix(channels): prioritize channel memory item updates
2026-07-15 00:44:15 +00:00
jinye
98f2bb37ec
feat(cli): Add runtime daemon channel control (#6741)
Some checks are pending
E2E Tests / web-shell Browser Regression (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(cli): add runtime daemon channel control

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address daemon channel review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(daemon): align channel control timeout budget

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): preserve serve fast-path import boundary

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): distinguish pending channel generations

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): use workspace env for deferred webhook auth

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6741)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6741)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6741)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6741)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6741)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6741)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6741)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-13 02:53:27 +00:00
qqqys
25f491d3ac
feat(dingtalk): mention response senders (#6679)
* docs: design DingTalk at-sender replies

* docs: plan DingTalk at-sender replies

* feat(channels): preserve session for response delivery

* feat(dingtalk): optionally mention response sender

* docs(dingtalk): explain response mentions

* fix(dingtalk): retain queued mention targets

* fix(dingtalk): bound mention target lifecycle

* fix(dingtalk): clear synthetic command mention target

* fix(dingtalk): clear buffered targets on session death

* debug(dingtalk): log mention delivery result

* fix(dingtalk): render response mentions

* fix(dingtalk): send visible response mentions

* feat(dingtalk): use text replies for mentions

* fix(dingtalk): preserve mentioned text replies
2026-07-11 00:08:13 +00:00
qqqys
7129cecba2
fix(channels): manage stale DingTalk Stream connections (#6675)
* fix(channels): manage stale DingTalk Stream connections

* fix(channels): harden DingTalk connection lifecycle

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-10 14:19:59 +00:00
qqqys
c84089ec48
fix(dingtalk): preserve markdown tables (#6673) 2026-07-10 13:08:31 +00:00
qqqys
043c22cb4b
feat(channels): support webhook-triggered channel tasks (#6495)
* feat(daemon): add a2a settings and capabilities

* docs(channels): design webhook-triggered tasks

* feat(channels): add webhook task helpers

* fix(channels): bound webhook prompt metadata

* feat(channels): run webhook-triggered tasks

* fix(channels): harden webhook task lifecycle

* fix(channels): require yolo for webhook tasks

* feat(channels): parse webhook configuration

* fix(channels): validate webhook secrets

* feat(channels): forward webhook tasks to channel worker

* fix(channels): require webhook enqueue on supervisors

* fix(channels): handle webhook IPC send failures safely

* feat(serve): accept channel webhook tasks

* fix(serve): stop webhook validation after first error

* fix(webhooks): reject inherited target refs

* docs(channels): document webhook-triggered tasks

* docs(channels): fix webhook task example

* docs(channels): refine webhook task docs

* docs(channels): add webhook task implementation plan

* fix(channels): restore webhook task context and chunks

* fix(serve): classify worker webhook enqueue failures

* fix(channels): address webhook review feedback

* fix(serve): address channel webhook review blockers

* fix(serve): satisfy channel webhook lint

* fix(serve): harden channel webhook admission

* fix(serve): narrow channel webhook source config

* fix(serve): classify webhook session scope failures

* fix(serve): harden webhook payload handling

* fix(serve): authenticate webhook startup cheaply

* fix(serve): keep deferred serve fast path lean

* fix(serve): address deferred webhook review blockers

* fix(channels): propagate webhook approval mode

* fix(channels): harden webhook task admission

* fix(acp): harden approval mode initialization

* fix(channels): harden webhook shutdown and secrets

* fix(channels): harden webhook review blockers

* fix(serve): harden deferred webhook auth

* test(channels): cover webhook target rejection

* fix(channels): preserve webhook thread targets

* fix(channels): address webhook review blockers

* fix(channels): harden webhook review blockers

* test(serve): align deferred webhook secret log assertion

* fix(channels): isolate webhook thread sessions

* fix(channels): harden webhook enqueue failures

* fix(serve): classify disabled channel workers
2026-07-10 12:54:27 +00:00
joeytoday
c0aeb7df5d
docs(channels): add setup screenshots to WeCom robot guide (#6648) 2026-07-10 12:10:06 +00:00
qqqys
2054302357
fix(channels): align memory access with channel gates (#6620)
Some checks are pending
E2E Tests / web-shell Browser Regression (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* fix(channels): align memory access with channel gates

* fix(channels): harden channel memory injection

* test(ci): align autofix workflow assertions
2026-07-10 00:00:41 +00:00
ChengHui Chen
1f92787aa0
feat(channels): add dmPolicy config to disable private/DM messages (#6521)
* feat(channels): add dmPolicy config to disable private/DM messages

Add DmGate class mirroring GroupGate to gate DM/private messages in
channel adapters. Operators can now set dmPolicy: 'disabled' in their
channel config to silently drop all DM messages while keeping group
messages active.

Closes #6392

* fix(channels): address review feedback for dmPolicy

- Add dmPolicy: 'open' to all test config factories (8 files) to
  maintain type correctness with required ChannelConfig field
- Add integration tests in ChannelBase.test.ts:
  - preflightInbound: DM dropped + group passes when dmPolicy=disabled
  - isStoredLoopTargetAuthorized: DM loop job disabled + group passes
- Add dmPolicy assertions in config-utils.test.ts (default + explicit)
- Keep dmPolicy as required field (not optional) for strict parity
  with groupPolicy
2026-07-08 12:01:10 +00:00
Dragon
394c1a289e
docs(channels): add WeCom to channels overview (#6490)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
The WeCom intelligent robot channel (added in #6436) has its own guide and _meta.ts entry, but the channels overview was never updated. Add WeCom to the platform list, quick-start guide links, the `type` option and `token` exclusion note, new `botId`/`secret` option rows, the media-support note, and the slash-command channel enumeration.

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-07 22:39:06 +00:00
qqqys
e3d7d10d1d
[codex] add natural channel memory intents (#6376)
* feat(channels): add natural channel memory intents

* fix(channels): add explicit guard and exhaustiveness check for clear_confirm intent

The clear_confirm path was handled as implicit fall-through at the bottom
of handleChannelMemoryIntent. If a new intent kind were added to the
ChannelMemoryIntent union, it would silently execute clearChannelMemory
without user confirmation — a data-loss risk.

Add explicit if (intent.kind === 'clear_confirm') guard and a
const _exhaustive: never assertion so TypeScript flags any unhandled
kinds at compile time.

* fix(channels): close session leak in classifier and fix regex separator

- BridgeChannelMemoryIntentClassifier now wraps prompt() in try/finally
  to always call cancelSession(), preventing daemon session leaks on
  every classifier invocation. Cleanup errors are caught so they cannot
  mask a successful classification result.
- Add missing optional punctuation separator to the 以后记住 regex
  pattern for consistency with other Chinese remember patterns.

* fix(channels): enforce pending clear state for channel memory confirmation

The clear_confirm intent executed clearChannelMemory directly without
verifying a prior clear_request was issued for the same chat. Any
authorized user could clear any chat's memory by sending the confirmation
phrase standalone, bypassing the two-step flow.

Add a per-target pending clear map (chatId + threadId, 60s TTL) that
is set during clear_request and verified+consumed during clear_confirm.
Standalone confirmation phrases now get rejected with a prompt to
issue the clear request first.

* fix(channels): include senderId in pendingClears key to prevent cross-user confirmation

User A could initiate clear_request in a group chat and User B could
confirm it, since the pending key only included chatId+threadId.
Add senderId to the key so only the user who initiated the clear
can confirm it.

* fix(channels): harden memory intent review fixes

* fix(channels): cover memory clear sender guard

* fix(channels): block group memory mutations

* fix(channels): avoid ambiguous memory saves

* test(channels): cover memory classifier cleanup

* test(channels): cover memory clear expiry

* fix(channels): restore channel memory slash aliases

* test(channels): cover memory intent edge cases
2026-07-07 15:25:01 +00:00
qqqys
467b292b50
feat(channels): add WeCom intelligent robot channel (#6436)
* feat(channels): add WeCom smart bot channel

* fix(channels): harden wecom review suggestions

* fix(channels): address wecom critical review

* fix(channels): include wecom mixed voice text

* fix(channels): tighten wecom outbound media

* fix(channels): harden wecom outbound sends

* fix(channels): address wecom review blockers

* fix(channels): address wecom review followups

* fix(channels): harden wecom inbound handling

* fix(channels): address wecom auth and media review

* fix(channels): tighten wecom inbound cleanup

* fix(channels): harden wecom media safety

* fix(channels): address wecom review typecheck

* fix(channels): harden wecom media review gaps

* fix(channels): address wecom review blockers

* fix(channels): tighten wecom media edge cases

* fix(channels): address wecom review blockers

* fix(channels): address wecom media review blockers

* fix(channels): address wecom review follow-ups

* fix(channels): address wecom review blockers

* fix(channels): close wecom review blockers

* fix(channels): close wecom preflight dedup race

* fix(channels): close wecom review gaps

* fix(channels): harden wecom kick reconnect

* fix(channels): defer wecom session resolution

* fix(channels): clean wecom session attachments

* fix(channels): harden wecom reconnect and media cleanup

* fix(channels): address wecom review diagnostics

* fix(channels): improve wecom diagnostics

* fix(channels): reset wecom kick retries

* fix(channels): improve wecom diagnostics

* fix(channels): preserve sync cancel preflight

* fix(channels): close wecom connection and ssrf gaps

* fix(channels): clean coalesced wecom attachments

* fix(channels): bound wecom sdk connect wait

* fix(channels): scope wecom untracked attachment cleanup

* fix(channels): block wecom nat64 local-use ssrf

* fix(channels): harden wecom media handling

* fix(channels): harden wecom group gates

* fix(channels): bound wecom kick reconnect cycles

* fix(channels): drain loop collect prompts directly

* fix(channels): align wecom buffer hooks

* fix(channels): harden wecom delivery failures

* fix(channels): recover from wecom attachment write failures

* fix(channels): surface wecom media send failures

* fix(channels): harden wecom replay and reconnect

* fix(channels): clarify wecom partial delivery cleanup

* fix(channels): close wecom rejected downloads

* fix(channels): retain wecom dedup after processing starts

* fix(channels): harden wecom reconnect and media errors

* fix(channels): add wecom media error context

* fix(channels): improve wecom dns diagnostics

* fix(channels): keep wecom kick retry alive

* fix(channels): allow wecom quoted bot replies

* fix(channels): preserve wecom code fences across chunks

* fix(channels): harden wecom reconnect lifecycle

* fix(channels): report wecom media dir setup failures

* fix(channels): harden wecom reconnect recovery

* fix(channels): align wecom review fixes

* fix(channels): harden wecom marker parsing

* fix(channels): keep wecom reconnect timers alive

* fix(channels): handle wecom tilde fences

* fix(channels): preserve wecom fence state

* fix(channels): clean up wecom attachment races

* fix(channels): bind wecom media reads to file handles

* fix(channels): prevent wecom symlink media opens

* fix(channels): address wecom review blockers

* fix(wecom): remove media URL from error messages to prevent credential leakage

The guardedHttpsDownload error messages included rawUrl (truncated to 120
chars), which leaks private WeCom media download URLs into stderr and log
aggregation systems. Remove the URL from redirect and HTTP error messages.

* fix(wecom): address review feedback — tests, security, correctness

- Remove stale URL assertions from media download error tests (the error
  messages no longer include raw URLs after the credential-leak fix)
- Redact sensitive fields (secret, aeskey, token, password, authorization)
  in formatSdkError's JSON.stringify fallback to prevent credential
  leakage in logs
- Add indented code block detection to findCodeRanges so [IMAGE: path]
  inside 4-space/tab-indented code is not stripped as a media marker
- Add disconnectGeneration guard before mkdirSync in downloadAttachments
  to prevent orphaned temp directories when disconnect() races with
  in-flight attachment downloads

* fix(wecom): wrap client.disconnect() in catch block to preserve connection error

In the connect() catch block, client.disconnect() could throw (e.g. if
the WebSocket was already destroyed), masking the original connection
error. Wrap in try/catch so cleanup failures never shadow the root cause.

* fix(channels): address wecom reconnect review blockers

* fix(channels): harden wecom reconnect review fixes

* fix(channels): harden wecom review blockers

* fix(channels): address wecom review blockers

* fix(channels): preserve unsupported wecom media markers

* fix(channels): address wecom reliability suggestions

* fix(channels): allow wecom retry after early drops

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-07 15:24:19 +00:00
Dragon
fa81e0a604
docs: fix skill invocation syntax and include Feishu in channel lists (#6320)
* docs: fix skill invocation syntax and include Feishu in channel lists

* docs: add Feishu column to channel media-handling table

Address review feedback on PR #6320: after adding Feishu to the prose
channel lists, the 'Platform differences' media-handling table still
omitted it. Add a Feishu column (images/files via the authenticated Open
API resources endpoint, 50MB limit; rich-text 'post' captions), verified
against packages/channels/feishu/src/media.ts and FeishuAdapter.ts. Add a
note that QQ Bot ignores incoming media (per QQChannel.ts) so it has no
row.

* docs: clarify Feishu post messages drop embedded images

The Platform differences table claimed Feishu rich-text (post) messages
carry 'mixed text + images', but FeishuAdapter's post parser only
extracts text/a/at nodes and silently drops img nodes (both the live
handler and the history-backfill path). Correct the Captions cell to
say text is extracted and embedded images are ignored.

* docs: mark clientId/clientSecret as required for Feishu too

Feishu declares requiredConfigFields: ['clientId', 'clientSecret']
(packages/channels/feishu/src/index.ts), same as DingTalk, but the
channel options table listed both fields as DingTalk-only. Update the
Required column and descriptions to cover Feishu (App ID / App Secret).

* docs: note token is not needed for Feishu in channel config table

* docs: note 50MB limit on Feishu image downloads

Feishu images and files both flow through the same downloadMedia() in
packages/channels/feishu/src/media.ts, which enforces a single
MAX_DOWNLOAD_BYTES = 50MB cap. Add the (50MB limit) note to the Images
cell for consistency with the Files cell.

* docs: clarify /skills panel-vs-run behavior per review feedback

- skills.md: add a migration Note that /skills <name> now opens the
  Skills panel and ignores trailing args; use /<skill-name> to run.
- commands.md: list the /skills Usage cell as /skills, /<skill-name>
  for consistency with the rest of the table.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: 易良 <1204183885@qq.com>
2026-07-05 09:49:52 +00:00
qqqys
6a5ad453c8
[codex] Add explicit channel memory for messaging channels (#6051)
* chore: ignore local worktrees

* feat(core): add channel memory store

* fix(core): preserve dots in channel memory paths

* fix(core): guard dot channel memory paths

* feat(channels): add channel memory commands

* fix(channels): defer channel memory session invalidation

* feat(channels): inject channel memory into sessions

* fix(channels): serialize channel memory context injection

* fix(channels): harden channel memory context races

* fix(channels): let queued turn retry memory context

* feat(cli): wire channel memory into channel startup

* docs(channels): document channel memory

* fix(channels): harden channel memory handling

* fix(channels): gate channel memory injection

* fix(channels): serialize channel memory clears

* fix(channels): tolerate channel memory read failures

* fix(channels): harden channel memory review gaps

* fix(channels): harden channel memory review gaps

* fix(channels): protect shared channel memory

* test(channels): allow group memory view test

* fix(channels): block group memory writes

* fix(channels): include memory in loop prompts

* fix(channels): preserve session context order after merge

* fix(channels): retry loop memory context after read failure

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-01 09:59:48 +00:00
qqqys
891c59fbaa
feat(channels): add group history backfill (#6074)
* feat(channels): add group history backfill

* fix(channels): harden group history backfill

* fix(channels): address group history review gaps

* fix(channels): apply wildcard group history limit
2026-07-01 16:24:09 +08:00
jinye
cf6323bfb5
feat(cli): Add daemon-managed channel worker for serve --channel (#6031)
* feat(cli): add daemon-managed channel worker

* codex: address PR review feedback (#5978)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): harden serve channel worker lifecycle

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): cover channel worker edge cases

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address channel worker review followups

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): clear channel pidfile after worker exit

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address serve channel review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): harden serve channel worker review issues

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): preserve channel worker exit errors

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): harden daemon channel worker startup

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address channel worker review cleanup

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): track channel worker exit explicitly

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address daemon worker startup review (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address daemon worker disconnect review (#6031)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address serve channel review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): address channel worker review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-01 01:40:54 +00:00
jinye
c90e6e7ba4
feat(channels): Add channel agent bridge abstraction (#5978)
* feat(channels): add channel agent bridge abstraction

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): handle bridge session lifecycle cleanup

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(channels): close bridge lifecycle review gaps

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5978)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5978)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix: address channel bridge review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5978)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5978)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-29 10:03:28 +00:00
jinye
a234860a4a
fix(core): Align MCP OAuth guidance and docs (#5589)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* docs: Align docs with current CLI behavior

Update stale documentation and user-facing MCP OAuth guidance to match the current dialog-based flows, SDK permission semantics, current links, and Qwen OAuth status.

Also replace Ink internal imports with public Ink APIs for the shared text input so the workspace builds against Ink 7.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: fix BaseTextInput Ink import (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#5589)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): surface MCP OAuth credential read failures

Fix SSE OAuth credential pre-check failures by reporting token storage read errors before connecting.

Update SDK coreTools docs and extension release link text from the follow-up review.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): harden MCP OAuth error handling

Handle stderr warning failures as best-effort and keep SSE 401 OAuth guidance when credential re-read fails.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): keep SSE OAuth pre-read best effort

Avoid blocking SSE MCP connections when the diagnostic credential pre-read fails, and cover BaseTextInput absolute-position edge cases.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): handle SSE OAuth validation errors

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): surface MCP OAuth recovery guidance

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): cover MCP OAuth retry paths

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): address OAuth guidance review

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-24 07:09:53 +08:00
曹潇缤
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
2026-06-19 06:32:52 +08:00
joeytoday
f27e0afa45
docs(channels): add screenshots to Feishu setup guide (#4983) 2026-06-16 04:34:03 +00:00
yuanyuanAli
7732554805
feat(channels): add Feishu (Lark) channel adapter (#4379)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(channels): add Feishu (Lark) channel adapter

* fix(channels/feishu): fix webhook stop button, memory leak, spin-wait timeout, and reaction cleanup

* fix(channels/feishu): fix security, stability and build issues from PR review

* fix(channels/feishu): fix card lifecycle, streaming limits, and download safety from CR round 2

* fix(channels/feishu): harden webhook, card lifecycle, and disconnect cleanup from CR round 3

* fix(feishu): clarify stoppedMessages JSDoc to match actual cleanup behavior

* fix(channels/feishu): handle post messages without language key wrapper in quote context

* fix(channels/feishu): fix webhook signature bypass, stop-button double-send, and blockStreaming duplicates from CR round 4

* fix(channels/feishu): harden card lifecycle, markdown splitting, and defensive guards from CR round 5

* fix(channels/feishu): harden card lifecycle, markdown splitting, and defensive guards from CR round 5

- Set cardCreationFailed on onPromptStart failure to prevent retry spiral
- Skip throttle updates when card creation permanently failed
- Handle code fences in hard-split and table-stripping fallbacks
- Use parity-based fence detection in splitByTables (align with splitChunks)
- Add cs.stopped and else branch in onPromptEnd to prevent timer race and state leak
- Mark cardState.stopped after busy-wait timeout to abandon orphaned in-flight creation
- Apply MAX_CARD_CHARS truncation with fence parity in onResponseComplete
- Sanitize senderId before <at> tag interpolation
- Use replaceAll + callback form for mention replacement
- Floor token expiry to prevent thundering herd on expire:0
- Add log for stop-button auth rejection
- Fix stoppedMessages JSDoc to match actual cleanup lifecycle
- Fix test fixture to match "still creating" scenario
- Fix typecheck errors in test file (TS2571, TS4111)
- Add stop-button auth negative path tests (operator mismatch, missing operator, missing sender)
- Replace spanning regex in table-stripping with line-by-line stripTables() to resolve CodeQL ReDoS warning

* fix(channels/feishu): fix HMAC bypass, prompt injection, SSRF, and card lifecycle from CR round 5-6

Security:
- Fix webhook HMAC bypass: use defineProperty(non-enumerable) for headers instead of prototype shadowing
- Fix cross-user prompt injection: mark quoted content as untrusted with explicit marker
- Fix SSRF: validate all Feishu IDs with FEISHU_ID_RE before URL interpolation in 6 endpoints
- Fix safeSenderId regex: add hyphen to character class so ou_abc-def-123 is not rejected

Card lifecycle:
- Set cardCreationFailed on onPromptStart failure to prevent retry spiral
- Skip throttle updates when card creation permanently failed
- Fallback to plain message delivery when cardCreationFailed with accumulated text
- Track creationTimer in CardSessionState so cleanupCard/disconnect can cancel orphaned card creation
- Add cs.stopped and else branch in onPromptEnd to prevent timer race and state leak
- Mark cardState.stopped after busy-wait timeout to abandon orphaned in-flight creation
- Apply MAX_CARD_CHARS truncation with fence parity in onResponseComplete
- Preserve atPrefix in streaming truncation to prevent @mention visual snap
- Account for suffix and fence reserve in truncation maxBody calculation
- Clean up auxiliary maps after handleInbound when gate rejects the message
- Clean up blockStreaming mode Map entries in onPromptEnd
- Skip bare @mention without question text

Markdown:
- Handle code fences in hard-split and table-stripping fallbacks
- Use parity-based fence detection in splitByTables (align with splitChunks)
- Replace spanning regex in table-stripping with line-by-line stripTables() to resolve CodeQL ReDoS warning

Defensive guards:
- Sanitize senderId before <at> tag interpolation
- Use replaceAll + callback form for mention replacement
- Floor token expiry to prevent thundering herd on expire:0
- Add log for stop-button auth rejection

Tests:
- Fix stoppedMessages JSDoc to match actual cleanup lifecycle
- Fix test fixture to match "still creating" scenario
- Fix typecheck errors in test file (TS2571, TS4111)
- Add stop-button auth negative path tests (operator mismatch, missing operator, missing sender)
- Assert cancelSession called in stop-button happy-path test

* fix(channels/feishu): add request timeouts, token dedup, and harden file/quote sanitization

* fix(channels/feishu): harden card lifecycle, webhook auth, and resource cleanup from CR round 7

* fix(channels/feishu): harden card lifecycle, mention handling, and error recovery
2026-05-28 20:11:00 +08:00
tanzhenxin
7251da0152 feat(channels): add dispatch modes and prompt lifecycle hooks
Add three dispatch modes for handling concurrent messages:
- steer (default): cancel current prompt and start new one
- collect: buffer messages and coalesce into follow-up prompt
- followup: queue messages for sequential processing

Introduce onPromptStart/onPromptEnd lifecycle hooks for working
indicators. These fire only when a prompt actually begins processing,
not for buffered (collect mode) or gated/blocked messages.

Refactor Telegram, WeChat, and DingTalk adapters to use the new hooks
instead of overriding handleInbound, simplifying the working indicator
pattern and ensuring correct behavior with dispatch modes.

This enables better UX for async workflows and prevents indicator
leaks when messages are buffered or cancelled.
2026-03-28 06:19:02 +00:00
tanzhenxin
39103eea5f docs(channels): document attachments and block streaming features
- Add Attachments interface docs with handling examples
- Document block streaming configuration and behavior
- Update architecture diagrams to show attachment resolution
- Add Attachment type to exported types reference
- Update plugin-example README

Covers new structured attachment support and block streaming
that delivers responses as multiple progressive messages.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-27 14:57:17 +00:00
tanzhenxin
987eebd1c4 docs(channels): add plugin developer guide and rename mock to plugin-example
- Add comprehensive developer guide for building channel plugins
- Add user-facing docs for installing/configuring custom channel plugins
- Replace custom-channels.md with new plugins.md
- Rename @qwen-code/channel-mock to @qwen-code/channel-plugin-example
- Add messageId field to Envelope type for response correlation

This provides clear documentation for developers building custom channel
adapters and renames the mock package to better reflect its purpose as
a reference implementation example.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-27 03:19:34 +00:00
tanzhenxin
01c2e5a373 docs(channels): add custom channel plugins documentation
- Document channels config in extension manifest
- Add guide for creating custom channel adapters
- Explain ChannelPlugin interface and ChannelBase usage

This enables users to extend the channel system with custom platform adapters.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 14:41:20 +00:00
tanzhenxin
33901fb9cd docs(channels): add DingTalk channel documentation
- Add comprehensive DingTalk setup guide with prerequisites, configuration, and troubleshooting
- Add WeChat and DingTalk entries to channels navigation

This provides users with complete documentation for setting up and using DingTalk as a Qwen Code channel.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 09:14:31 +00:00
tanzhenxin
9c001ba61e feat(channels): add shared slash command system
- Add /help, /clear (aliases: /reset, /new), /status commands to ChannelBase
- Commands are handled locally without agent round-trip
- TelegramAdapter skips "Working..." indicator for local commands
- Update docs to reflect new command structure

This provides a consistent command interface across all channel types
(Telegram, WeChat, etc.) with platform-specific extensibility.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 03:24:44 +00:00
tanzhenxin
697898a9fb feat(channel): add status and stop commands for service management
- Add `qwen channel status` to check running service info
- Add `qwen channel stop` to gracefully stop the service
- Add PID file tracking to prevent duplicate service instances
- Update documentation with new commands and usage

This enables users to manage the channel service from another terminal
without needing to use Ctrl+C on the foreground process.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 03:06:48 +00:00
tanzhenxin
1a605ec973 feat(channels): add crash recovery and gateway mode support
- Add session persistence to SessionRouter for crash recovery
- Add loadSession method to AcpBridge for restoring sessions
- Add ChannelBaseOptions to support external router injection
- Refactor start.ts to support both standalone and gateway modes
- Extract config utilities into separate module

This enables channels to recover sessions after bridge crashes and
supports running multiple channels under a gateway process.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 02:55:18 +00:00
tanzhenxin
f6ae769736 docs(channels): document media support and add WeChat guide
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

- Add Media Support section to overview with images and files

- Document model option for multimodal channel support

- Add Images and Files section to Telegram guide

- Add complete WeChat (Weixin) setup guide with QR auth

This documents the new media handling capabilities added to both Telegram and WeChat channels.
2026-03-25 08:12:53 +00:00
tanzhenxin
90236465d3 feat(channels): add group chat support for Telegram
- Add GroupGate class for group access control with three policies:
  disabled (default), allowlist, and open
- Implement mention gating: bot only responds when @mentioned or replied to
  in groups (configurable per-group)
- Extend Envelope type with isGroup, isMentioned, isReplyToBot fields
- Update TelegramAdapter to detect group context and mentions
- Add comprehensive documentation for group chat setup and troubleshooting

This enables using Qwen Code bots in Telegram groups with fine-grained
access control and mention-based activation to prevent noise.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-25 02:04:27 +00:00
tanzhenxin
8753245b5f feat(channels): add DM pairing flow for sender approval
- Add PairingStore for managing pending requests and approved users
- Update SenderGate to support pairing policy with code generation
- Add CLI commands: `qwen channel pairing list/approve`
- Document pairing flow with rules and usage examples

This allows unknown senders to request access via a pairing code
that the bot operator approves through the CLI.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-24 11:37:16 +00:00
tanzhenxin
59ee49e0ab docs(channels): add Channels feature documentation
- Add overview page explaining channels architecture and configuration
- Add Telegram channel setup guide with bot creation steps
- Add navigation entries for channels section

This documents the new Channels feature that allows users to interact
with Qwen Code agents from messaging platforms like Telegram.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-24 11:12:52 +00:00