mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-18 05:04:45 +00:00
134 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b8b3287308
|
fix(channels): add chat payload diagnostics (#6539)
* fix(channels): improve wecom mention diagnostics * fix(channels): harden diagnostic logging * fix(channels): redact platform sender identities * test(channels): assert debug silence before restoring spies * fix(channels): log pairing-required preflight drops * fix(channels): compact debug payload logs * test(channels): expect compact debug payload logs |
||
|
|
b330ec884f
|
chore(release): v0.19.8 (#6549)
* chore(release): v0.19.8 * docs(changelog): sync for v0.19.8 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
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 |
||
|
|
58e51eb96c
|
fix(channel): Relay ACP permission requests (#6446)
* fix(channel): Relay ACP permission requests * fix(channel): harden permission relay cleanup * fix(channel): scope ACP permission approvals * fix(channel): harden permission cancellation diagnostics * test(channel): cover permission lookup edge cases * fix(channel): close permission relay stale requests * fix(channel): tighten approve-always option matching * test(channel): cover permission relay cleanup gaps * fix(channel): deliver threaded permission prompts --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
86ae16a6d6
|
chore(release): v0.19.7 (#6484)
* chore(release): v0.19.7 * docs(changelog): sync for v0.19.7 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
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 |
||
|
|
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> |
||
|
|
b23f888d73
|
[codex] add proactive channel loop tools (#6287)
* feat(channel): add proactive loop tools * fix(channels): stabilize proactive loop routing * fix(channels): gate loop tools in shared sessions * fix(channels): tighten channel loop tool routing * fix(channels): close loop tool review blockers * fix(dingtalk): preserve markdown tables * fix(dingtalk): use app token for reactions * fix(channels): scope loop tools to active caller * fix(channels): preserve group session metadata * fix(channels): normalize loop targets * test(cli): cover settings cron disable path * fix(channels): address dingtalk review suggestions * fix(dingtalk): restore table normalization * fix(channels): mark loop tool failures * fix(channels): tighten loop mcp protocol handling * test(channels): cover loop tool guard paths * fix(channels): await loop mcp registration * test(channels): preserve base proactive target default * refactor(channels): clarify loop target promotion * fix(channels): harden loop recurring input * fix(channels): ack loop mcp notifications * fix(channels): preserve legacy loop targets * test(channels): cover channel loop wiring paths * fix(channels): retry skipped loop mcp registration * fix(channels): keep promoted loop targets visible * fix(channels): harden loop mcp input logging |
||
|
|
60b9c92b28
|
Restart stalled ACP bridge for channels (#6330)
* fix(channels): restart stalled ACP bridge * test(channels): derive acp stall thresholds * fix(channels): force kill stalled acp bridge * fix(channels): detect coalesced acp stall logs |
||
|
|
abec702ee2
|
fix(qqbot): streaming idle-flush with tool-call and stale-callback protection (#6204)
* fix(qqbot): streaming idle-flush with tool-call and stale-callback protection Add streaming infrastructure to QQ Bot channel: - streamState Map with per-session buffer and 2s idle-flush timer - onResponseChunk: accumulates text, resets timer on each chunk - idleFlush: flushes accumulated buffer, coordinates with pendingStreamDelete - onToolCall: flushes buffer before tool execution with double-send guard - onResponseComplete: defers streamState cleanup during flush - _reconnectId monotonic counter for stale async callback detection - blockStreaming config-driven guard (skip streaming when enabled) - All sendMessage calls chained with .catch() for Node 22+ safety Add 25 stream tests covering: - idle-flush accumulation and timer reset - onToolCall immediate flush - pendingStreamDelete coordination - Stale reconnect callback guard - blockStreaming guard - Concurrent flush prevention * fix(qqbot): streaming idle-flush with tool-call and stale-callback protection * fix(qqbot): address review feedback — duplicate-message, chunk-drop, propagate-error, retry-timer, escape-fix * fix(qqbot): add missing reference guards on streamState.delete * fix(qqbot): address PR #6204 review feedback - Extract IDLE_FLUSH_MS and MAX_FLUSH_RETRIES constants - Add JSDoc to state machine transitions - Implement onSessionDied lifecycle handler - Track retryCount per session for bounded retries - Sanitize log text via sanitizeLogText utility - Guard zombie timer with !current.timer check - Clean up flushedSessions and pendingStreamDelete entries - Add 8 error-recovery test cases (retry, max retries, pendingStreamDelete, onToolCall retry, stale closure, disconnect cleanup, onSessionDied, flushingSessions guard) - Fix vitest.config.ts server.deps.inline placement * refactor(qqbot): extract flushAndTrack helper, add 3 missing guard tests * fix(qqbot): fix flushedSessions tracking races, add buffer limit and test assertions * fix(qqbot): remove unused variable to fix ESLint error * fix(qqbot): restore 4 behaviors missing from PR-C vs feat comparison - Restore readyTimeout (30s READY guard) in dialGateway() - Restore heartbeatTimer.unref() in startHeartbeat() - Restore seenMessages.clear() in disconnect() - Restore event.author defensive check in handleC2C() * fix(qqbot): add identity guard to .finally(), rename _reconnectId, remove redundant chatId param * fix(qqbot): fix recursive flushAndTrack guard bypass and readyTimeout leak in disconnect * fix(qqbot): address 5 PR-C #6204 review threads - flush guard, stderr newlines, readyTimeout unref, reconnect log --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
4e3fd29781
|
chore(release): v0.19.6 (#6280)
* chore(release): v0.19.6 * docs(changelog): sync for v0.19.6 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
67da78166b
|
fix(qqbot): markdown-first send, replyMsgId TTL, and dead code removal (#6201)
* fix(qqbot): markdown-first send with replyMsgId TTL and dead code removal
- Change replyMsgId from Map<string,string> to Map<string,{msgId,timestamp}>
with 5-minute TTL and periodic cleanup timer
- Add setReplyMsgId() helper with cascaded msgSeqMap cleanup
- Rewrite sendMessage(): markdown-first (msg_type:2), active retry
on non-4xx failure, plain-text fallback for active messages
- Re-throw errors for .catch() callers instead of silent break
- Update restoreQQState() with backward-compatible replyMsgId migration
- Remove dead code exports: hasMarkdownSyntax, hasLinkSyntax, splitText
- Update send.test.ts: TTL expiry, markdown fallback, noreply suppression,
replyMsgId helper tests
* fix(qqbot): address review feedback — seq gap, 429 short-circuit, state persistence, input validation
- Fix msg_seq gap on active retry: rollback to nextSeq-1 sends nextSeq, not nextSeq+1
- Add race guard: check replyMsgId still current before updating msgSeqMap on success
- Short-circuit on 429 early: bail after markdown 429 instead of retrying
- Log MESSAGE DROPPED and persist state when both passive+active send fail
- Drain response body on plain-text fallback to prevent socket leak
- Persist after cleanup timer eviction (saveQQState)
- Validate msgSeqMap entries as [string, number] in restoreQQState
- Add Number.isFinite guard for timestamp in restoreQQState
- Eagerly delete expired replyMsgId entries on first TTL check
- Remove redundant saveQQState() calls after setReplyMsgId (handles itself)
- Fix instruction string: remove stale auto-chunk mention (splitText removed)
- Fix misleading log: expired reply says 'without msg_id' not 'active message'
* fix(qqbot): align sendMessage fallback and replyMsgId cleanup with feat branch
* fix(qqbot): address PR #6201 review comments — setReplyMsgId guard, cleanup persistence, TTL constant, test coverage
* fix(qqbot): address PR #6201 review round 2
* fix(qqbot): address PR #6201 review round 3 — add MESSAGE DROPPED prefix to plain-text fallback error log
* fix(qqbot): address PR #6201 review round 4
- Fix catch block: always call saveQQState() regardless of rollbackApplied
- Plain-text fallback log now includes error body text
- Add success logs for active retry and plain-text fallback paths
- Add .unref() to seenCleanupTimer for clean process exit
- Add threat-model comment to group sender-name sanitization tests
- Add saveQQState spy assertions to rollback tests
* fix(qqbot): address PR #6201 review round 5
* fix(qqbot): address PR #6201 review round 6
* fix(qqbot): address PR #6201 review round 8
- Guard far-future timestamps in restoreQQState validation with an upper
bound (now + REPLY_MSG_ID_TTL_MS) so corrupted state cannot pin entries
permanently.
- Add test for 429 without msgId returning silently (no fallback/rollback).
- Add test for 429 on plain-text fallback rate-limited path.
- Add test for setReplyMsgId same-msgId guard no-op branch (no delete).
|
||
|
|
c1235d8c69
|
fix(qqbot): security hardening — gateway validation, atomic state, sanitized logging (#6200)
* fix(qqbot): validate gateway URL protocol to prevent SSRF
- Add validateGatewayUrl(): enforce wss:// protocol, warn on unexpected hostnames
- Integrate into fetchGatewayUrl() return path
- Truncate error body in fetchAccessToken() to 80 chars
- Add 6 tests covering protocol rejection, wss acceptance, and edge cases
* fix(qqbot): atomic state persistence and error log sanitization
State persistence hardening:
- Atomic saveQQState() via tmp+renameSync with disposed guard and unref()
- Atomic flushQQState() with {mode: 0o600} permissions
- Entry type validation in restoreQQState() for chatTypeMap and msgSeqMap
Error log sanitization:
- Wrap all user-controlled data in process.stderr.write() with sanitizeLogText()
- Covering: connect retry, sendMessage errors, state persistence failures,
token refresh, malformed gateway, WebSocket errors, reconnect, and
C2C/group handler error paths
* fix(qqbot): add replyMsgId validation in restoreQQState()
Add type/length validation for replyMsgId entries when restoring from
persisted state, consistent with the existing chatTypeMap and msgSeqMap
input validation filters. Entries must be strings ≤ 128 chars.
* fix(qqbot): add test coverage for restoreQQState filters, atomic writes, and gateway URL validation
* docs(qqbot): update restoreQQState doc and add disposed guard comment
- Update restoreQQState JSDoc: document validation instead of "trusts persisted JSON"
- Add inline comment explaining why saveQQState has disposed guard but flushQQState doesn't
* docs(qqbot): update validateGatewayUrl docstring to reflect TLS enforcement, not SSRF
* fix(qqbot): address wenshao review — hostname rejection, error sanitization, msgSeqMap validation
* fix(qqbot): address PR review — drain body, narrow gateway hostname
- Drain resp.body?.cancel() in fetchAccessToken error to prevent
Undici connection leaks on repeated token failures
- Narrow validateGatewayUrl hostname check from broad Tencent
wildcards (.tencent.com, .tencentcs.com) to only *.qq.com
to prevent attacker-controlled Tencent Cloud API Gateway
domains from passing validation
* test(qqbot): tighten token-error assertion to exact match
* test(qqbot): add connect retry sanitization + msgSeqMap edge-case tests
Add test verifying the final connect() retry sanitizes newline/control
characters in the thrown error message. Add tests for fractional,
overflow, and Infinity values in msgSeqMap restore validation to
prevent regression of the Number.isSafeInteger fix.
* fix(qqbot): address wenshao review round 3 — error preservation, validation logging, URL normalization
* test(qqbot): fix connect retry sanitization assertion — sanitizeLogText preserves readable content, not censor words
* fix(qqbot): address wenshao review — error preservation, validation logging, URL userinfo stripping, test coverage
* fix(qqbot): add Array.isArray guards and replyMsgId drop logging in restoreQQState
Prevent TypeError from .filter() on non-array state values (e.g. object
from partial write). Missing Array.isArray() guard caused all three maps
to be lost on a single corrupted section — now each map independently
validates with both truthiness + Array.isArray() before filtering.
Also add replyMsgId drop-count logging (was missing while chatTypeMap
and msgSeqMap already had it).
* fix(qqbot): drain response body in token error path, sanitize URL TypeError, clean tmp on atomic write failure
Three fixes from wenshao review:
1. fetchAccessToken: cancel unconsumed response body to prevent Undici TCP
socket leak (could exhaust connection pool over hours in long-running daemon)
2. validateGatewayUrl: strip raw URL from TypeError message to prevent
log injection via malformed URL strings
3. saveQQState/flushQQState: unlinkSync(tmpPath) in catch blocks to
prevent orphaned .tmp files when renameSync fails (cross-device, Docker)
* fix(qqbot): test Infinity rejection via 1e999 raw JSON, not JSON.stringify nullification
* fix(qqbot): address PR #6200 review — beforeExit hook, key validation, error clarity
- Add beforeExit hook to flush debounced state on abnormal process exit
(SIGKILL, OOM, crash) when unref'd 500ms timer has pending writes
- Validate map keys (typeof string, ≤256 chars) in restoreQQState filters
alongside existing value validation to prevent non-string key bloat
- Improve gateway hostname error message to include expected domain
* fix(qqbot): address PR #6200 review round — non-object JSON restore, disposed guard, beforeExit dedup, comment fix
* fix(qqbot): hoist tmpPath declaration before try block in saveQQState
* fix(qqbot): update beforeExit JSDoc to accurately describe behavior
* fix(qqbot): drain response body in fetchGatewayUrl error path
|
||
|
|
2126474c28
|
chore(release): v0.19.5 (#6194)
* chore(release): v0.19.5 * docs(changelog): sync for v0.19.5 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
6509e8de08
|
feat(channels): show lifecycle status in adapters (#6114)
* chore: ignore local worktrees * docs(channels): design identity and task lifecycle p0 * docs(channels): plan identity and task lifecycle p0 * feat(channels): add identity and task lifecycle metadata * fix(channels): suppress cancelled tool call lifecycle * docs: add channel lifecycle status adapter design * docs: add channel lifecycle status adapter plan * feat(channels): map telegram lifecycle to typing * feat(channels): map weixin lifecycle to typing * fix(channels): reset weixin typing state after failed start * feat(channels): map dingtalk lifecycle to reactions * feat(channels): add feishu card status labels * fix(channels): preserve feishu collapsible status labels * feat(channels): show feishu lifecycle card status * fix(channels): cover loop lifecycle metadata * fix(channels): preserve feishu terminal card status * chore: remove internal task reports from branch * fix(channels): clear late lifecycle status starts * fix(channels): harden lifecycle event edges * fix(channels): clean adapter lifecycle state * fix(channels): finalize cancelled lifecycle before cleanup * fix(feishu): keep cancelled card status visible * docs(channels): add lifecycle status no-op coverage * fix(channels): address lifecycle review suggestions * docs(channels): align lifecycle status documentation * fix(channels): route adapter stop through lifecycle cancel * fix(channels): close lifecycle cancellation races * fix(channels): keep first feishu terminal status * fix(telegram): guard lifecycle typing updates * test(qqbot): cover lifecycle status no-ops * fix(feishu): preserve completed card race status * docs(lifecycle): align status review docs * fix(channels): separate pending cancel state * fix(channels): clean adapter lifecycle status edges * fix(channels): order clear cancellation lifecycle * fix(feishu): preserve user stop status label * fix(channels): suppress loop chunks during pending cancel * test(channels): use active session in cancel regression * fix(channels): harden adapter cancellation tests * fix(channels): sanitize lifecycle tool fields * fix(channels): route shared tool call lifecycle * fix(channels): preserve pending cancel intent * fix(channels): preserve pending cancel intent * fix(telegram): track typing by session * fix(feishu): preserve stop status during finalization * fix(channels): preserve responses after failed cancel * fix(channels): tighten lifecycle cancellation reasons * fix(channels): close lifecycle cancel races and validate identity config Address the outstanding review findings on #6105: - carry a typed reason on ChannelLoopSkippedError and report disabled loops as 'dropped' instead of 'timeout' - treat a turn as committed once delivery starts: /cancel re-checks deliveryStarted after the cancel RPC settles, and neither prompt path lets a late-settling cancel rewrite a delivered turn into cancelled (or follow a /clear cancellation with completed) - tag failed lifecycle events with phase: agent vs delivery - validate identity/memoryScope shape at config parse time instead of throwing an opaque TypeError on the first prompt of every session - guard onPromptStart hooks, pass job.id to loop onPromptStart/End, append the boundary block after operator instructions, gate /who + /status identity lines on configured identity/memoryScope, cache the boundary prompt, and route error logs through lifecycleError() Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(qqbot): keep lifecycle mock in sync * fix(channels): unify cancel state machine and adapter lifecycle edges Address the remaining review findings on #6114: - /cancel now delegates to requestActivePromptCancellation — one cancel state machine for slash command and adapter stop buttons; the helper refuses to claim success once delivery started and sanitizes its logs - share isTerminalTaskLifecycleType from channel-base instead of four hand-rolled terminal checks - DingTalk: only attach reactions for message ids seen inbound (loop job ids no longer trigger doomed emotion API calls), log reaction API failures, and recall reactions when a session dies - Feishu: track userStopped on the card state so every wind-down path renders 已停止生成 after a Stop click, and pass terminal labels through updateCard's statusLabel param at the remaining baked-text sites - Weixin: guard the typing .then() against a racing disconnect - document onTaskLifecycle as the canonical hook (onPromptStart/End are back-compat), fix TS4111 bracket access in the DingTalk test Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(channels): keep failed terminal event when cancel settles post-delivery Self-review follow-up: once delivery started, the catch paths no longer reconcile a pending cancel — a late-resolving cancel RPC used to flip cancelled=true there, suppressing the failed emit while the /cancel handler (seeing deliveryStarted) also declined to emit, leaving a started task with no terminal lifecycle event at all. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(channels): close self-review findings on adapter lifecycle edges - DingTalk: track inbound message ids in a capped insertion-ordered set instead of the TTL-swept dedup map, so a turn queued minutes behind a long predecessor still attaches its reaction - Feishu: reset userStopped when the cancel RPC fails (later wind-down must render the real terminal status), and give handleStop's plain message fallback the same ---/label shape the strip regex expects Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(channels): hold streamed chunks while a cancel is pending Chunks arriving while a /cancel RPC is in flight were pushed straight into the BlockStreamer, which can send a block on a size/paragraph threshold before the cancel resolves — leaking output a successful cancel can never recall. Hold the pending-window chunks instead: replay them (block streaming + text_chunk transcript) when the cancel fails, discard them when it succeeds. onResponseChunk stays live through the window so adapter-accumulated display state has no permanent hole on a failed cancel; adapters gate visible updates on their own stop flags. Also stop passing the loop job id to onPromptStart/onPromptEnd (and the /clear eviction path): the hook contract is inbound platform message ids, and adapters act on them — cards and reactions keyed to a fake id. Lifecycle events still carry job.id for correlation. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(feishu): strip every status-label layout from quoted-reply context Replace the two $-anchored strip regexes in extractCardText with one line-granular status-block filter. The anchored patterns missed four layouts the cards actually render: the two-block truncation card (notice block + label block), terminal labels joined mid-string before a collapsible panel body, the 停止失败,请重试 label (never in the alternation), and label-only stopped cards where the divider leads the text. Tests now assert the real rendered shapes. Also update the adapter-cancellation suppression test to the new hold-and-replay chunk semantics from the base layer. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(feishu): ignore loop job ids in prompt hooks * fix(channels): defer adapter chunks while cancel is pending * fix(feishu): preserve bare status text in quotes * fix(channels): release held chunks on failed cancel * fix(feishu): preserve generated stop fallback status * fix(telegram): clear typing on dead sessions * fix(feishu): share status label definitions --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
4b372d39ec
|
feat(channels): add listSessions to ChannelAgentBridge (#6182)
Add optional `listSessions()` method to `ChannelAgentBridge` interface so channel consumers can enumerate sessions currently attached to the bridge. Only `DaemonChannelBridge` implements it — reads from the internal `sessions` Map and `activePrompts` Set to build a snapshot. The daemon-worker facade forwards the method unconditionally when present, matching the existing `getAvailableCommands` pattern. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) |
||
|
|
ca61d7827e
|
feat(channels): add identity and task lifecycle metadata (#6105)
* chore: ignore local worktrees * docs(channels): design identity and task lifecycle p0 * docs(channels): plan identity and task lifecycle p0 * feat(channels): add identity and task lifecycle metadata * fix(channels): suppress cancelled tool call lifecycle * fix(channels): cover loop lifecycle metadata * fix(channels): harden lifecycle event edges * fix(channels): finalize cancelled lifecycle before cleanup * fix(channels): address lifecycle review suggestions * fix(channels): close lifecycle cancellation races * fix(channels): separate pending cancel state * fix(channels): order clear cancellation lifecycle * fix(channels): suppress loop chunks during pending cancel * test(channels): use active session in cancel regression * fix(channels): sanitize lifecycle tool fields * fix(channels): route shared tool call lifecycle * fix(channels): preserve pending cancel intent * fix(channels): preserve responses after failed cancel * fix(channels): tighten lifecycle cancellation reasons * fix(channels): close lifecycle cancel races and validate identity config Address the outstanding review findings on #6105: - carry a typed reason on ChannelLoopSkippedError and report disabled loops as 'dropped' instead of 'timeout' - treat a turn as committed once delivery starts: /cancel re-checks deliveryStarted after the cancel RPC settles, and neither prompt path lets a late-settling cancel rewrite a delivered turn into cancelled (or follow a /clear cancellation with completed) - tag failed lifecycle events with phase: agent vs delivery - validate identity/memoryScope shape at config parse time instead of throwing an opaque TypeError on the first prompt of every session - guard onPromptStart hooks, pass job.id to loop onPromptStart/End, append the boundary block after operator instructions, gate /who + /status identity lines on configured identity/memoryScope, cache the boundary prompt, and route error logs through lifecycleError() Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(channels): keep failed terminal event when cancel settles post-delivery Self-review follow-up: once delivery started, the catch paths no longer reconcile a pending cancel — a late-resolving cancel RPC used to flip cancelled=true there, suppressing the failed emit while the /cancel handler (seeing deliveryStarted) also declined to emit, leaving a started task with no terminal lifecycle event at all. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(channels): hold streamed chunks while a cancel is pending Chunks arriving while a /cancel RPC is in flight were pushed straight into the BlockStreamer, which can send a block on a size/paragraph threshold before the cancel resolves — leaking output a successful cancel can never recall. Hold the pending-window chunks instead: replay them (block streaming + text_chunk transcript) when the cancel fails, discard them when it succeeds. onResponseChunk stays live through the window so adapter-accumulated display state has no permanent hole on a failed cancel; adapters gate visible updates on their own stop flags. Also stop passing the loop job id to onPromptStart/onPromptEnd (and the /clear eviction path): the hook contract is inbound platform message ids, and adapters act on them — cards and reactions keyed to a fake id. Lifecycle events still carry job.id for correlation. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(channels): defer adapter chunks while cancel is pending --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
727bd47539
|
feat(channels): add DingTalk proactive send for channel loops (#6174)
DingTalk could only reply through the per-message sessionWebhook, so channel loops (#6073) rejected /loop add on DingTalk and could never deliver scheduled results cold. Implement proactive send via the robot groupMessages API with a self-managed access token (the stream SDK only refreshes on connect, so a long-lived socket serves a stale token after ~2h), a 15s request timeout so a hung send cannot freeze the session queue, and errors that carry the DingTalk error detail so /loop inspect is diagnosable. Group targets only; DM and webhook-URL fallback targets are rejected up front. Also give the loop prompt a delivery-contract line so the model treats a scheduled turn as content to produce rather than an action it must send itself — without it the model went hunting for a webhook to post through. Closes #6168 Co-authored-by: Qwen-Coder <noreply@qwen.ai> |
||
|
|
227a60cc93
|
fix(channels): replace setTimeout(0) drain with turn_complete SSE barrier (#6165)
* fix(channels): replace setTimeout(0) drain with turn_complete SSE barrier DaemonChannelBridge.prompt() used a setTimeout(0) heuristic to wait for late SSE text chunks after session.prompt() resolved. This was unreliable because it depended on microtask timing rather than a deterministic signal. Replace with a turn barrier that awaits the turn_complete SSE event from handleEvent, which guarantees all preceding session_update events (text chunks) have been processed. Also handle turn_error by logging the error data and resolving the barrier. Add resolveTurnBarrier calls in both dropSession and cancelSession to prevent deadlocks when sessions terminate while prompt() is awaiting the barrier. * fix(channels): resolve turn barrier before session.cancel() in cancelSession Move resolveTurnBarrier and abortActivePrompts before the await session.cancel() call, consistent with dropSession ordering. This prevents the barrier from hanging if cancel() blocks on a network timeout. * fix(channels): race turn barrier against setTimeout fallback for non-SSE paths DaemonSessionClient.prompt() can return directly without turn_complete in non-SSE paths (blocking HTTP when subscriptionActive is false, non-202 responses). The barrier would hang in these cases. Use Promise.race so turn_complete wins deterministically on the SSE path (microtask beats macrotask) while setTimeout(0) provides a safe fallback for non-SSE paths. Also update the turn_error test to match real SDK behavior where session.prompt() rejects on turn_error via _dispatchTurnEvent. |
||
|
|
487fb45510
|
feat(cli): Harden daemon-managed channel worker (#6098)
* feat(cli): harden daemon-managed channel worker Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6098) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6098) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6098) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6098) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6098) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): address channel worker review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): harden channel worker log supervision Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): redact channel worker snapshot errors Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * perf(cli): precompute channel worker log redactors Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): escalate permanent channel worker failures Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): bound oversized worker log tail discard Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6098) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6098) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6098) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * codex: address PR review feedback (#6098) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
f3ea17bf43
|
chore(release): v0.19.4 (#6132)
* chore(release): v0.19.4 * docs(changelog): sync for v0.19.4 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
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> |
||
|
|
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 |
||
|
|
c958e6f6f9
|
feat(channel): add channel loop support (#6073)
* feat(channel): add proactive routines * fix(channel): harden scheduled routines * fix(channel): report Feishu proactive send failures * fix(channel): reject unsupported threaded routines * fix(channel): harden routine scheduler lifecycle * fix(channel): enforce routine quotas atomically * feat(channel): surface routine lifecycle * test(channel): align routine job fixtures * fix(channel): initialize routine lifecycle fields * fix(channel): harden routine scheduler concurrency * fix(channel): evict timed out scheduled sessions * fix(channel): harden routine lifecycle review gaps * feat(channel): add channel loop support * test(channel): fix loop scheduler lint * fix(channel): harden loop lifecycle checks * test(channel): fix loop scheduler lint * fix(channel): harden loop review handling * fix(channel): harden loop timeout handling * fix(channel): cap loop scheduler concurrency * fix(channel): harden loop session recovery * fix(channel): harden loop lifecycle recovery * fix(channel): harden loop recovery gaps * fix(telegram): preserve injected bot on first connect |
||
|
|
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> |
||
|
|
ceb1d307a1
|
fix(channels): structure DingTalk stream logs (#5998)
* fix(channels): structure DingTalk stream logs * fix(channels): harden dingtalk downstream handling * fix(channels): harden dingtalk downstream fields * fix(channels): address dingtalk review blockers * fix(channels): validate dingtalk downstream routing * fix(ci): add serve fast path bundle check |
||
|
|
c5fb9fd1e7
|
fix(ci): create isolated home before tests (#6071) | ||
|
|
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> |
||
|
|
68348e236a
|
feat(channels): qwen tag — RFC + Phase 0 (multiplayer channel-resident agent) (#5888)
* docs(channels): add "qwen tag" RFC — channel-resident multiplayer agent Design for a persistent, multiplayer agent that lives in a chat channel (DingTalk-first), built on the existing channel adapters (qwen channel start, packages/channels/*) and the qwen serve daemon rather than a new service. Covers the phased plan: Phase 0 multiplayer identity, Phase 1 proactive engine (scheduler + cold-group push, daemon migration), Phase 2 channel memory + governance, plus tradeoffs and resolved design decisions. Part of #5887 Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * feat(channels): qwen tag Phase 0 — multiplayer identity & group-safe commands In a thread-scoped group every member shares one session, which surfaces a few gaps in the channel layer. Phase 0 closes them as a backward-compatible increment on the existing AcpBridge channel path: - Inject a [sender] marker into the prompt for group turns so the agent can tell speakers apart; skipped for 1:1 chats and for already-prefixed re-entries. - Add Envelope.alreadyPrefixed so collect-mode coalescing does not double-prefix the already-tagged buffered text. - Require "/clear confirm" in groups — a bare /clear no longer wipes the shared session for the whole channel; DMs still clear directly. - Add a read-only /who reporting channel / workspace / session scope without creating a session. - Drop DingTalk group messages with no conversationId, so the shared session is never keyed on the expiring sessionWebhook. - Fix the stale ChannelConfig.dispatchMode JSDoc (runtime default is 'steer'). Adds unit tests for sender attribution, the collect double-prefix guard, the group /clear confirmation, and the read-only /who. Part of #5887 Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): scope group clear confirmation * fix(channels): address review on qwen tag Phase 0 - Restrict /clear in a shared (thread) group to config.allowedUsers (when set) and require an explicit "confirm"; only the shared case is gated — DMs and per-user groups clear directly. Reconcile the RFC (OD-4) with this approach (a hyphenated /clear-channel isn't parseable; per-member owner-gate waits on the identity model, OD-3/OD-11). - Sanitize the injected [sender] marker (strip brackets/CR/LF, cap length) so a crafted nick can't break out of or spoof the attribution tag. - Surface to stderr when a collect-mode coalesced re-entry drops buffered turns instead of swallowing the error. - Narrow Envelope.alreadyPrefixed to the literal `true` (internal-only flag). - Expose DingtalkChannel.isUnroutableGroupMessage and test the group-without-conversationId drop; replace flaky setTimeout sequencing in the collect test with vi.waitFor; add tests for /who (active + private scope) and /clear authorization in a shared group. Part of #5887 Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): preserve group command routing * fix(channels): harden group routing (map leak, injection, slash attribution) Address the surviving Phase-0 review criticals: - /clear now purges every per-session map (sessionQueues, activePrompts, collectBuffers), not just instructedSessions, so a long-running gateway doesn't leak dead session entries. - QQ group adapter sanitizes the self-prefixed sender name. It sets alreadyPrefixed, which bypasses ChannelBase's [..]/newline/length guard, so a crafted QQ nickname could otherwise inject brackets/newlines. - Unrecognized slash commands in a shared group keep their [sender] attribution; recognized commands (a local handler or a forwarded agent command) still reach the agent verbatim. - Sanitize quoted referencedText (strip control chars, cap at 500) so it can't inject newlines/instructions or balloon the prompt. - /who reports only the workspace basename, not the absolute cwd. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): close Phase-0 review criticals (quote injection, /clear cancel, drain log) - referencedText: strip the wrapper's own delimiters (" [ ]) in addition to control chars, so a quoted message can't break out of [Replying to: "..."] and inject its own top-level instructions. - /clear: cancel any in-flight bridge.prompt() for the cleared session(s) and drop their buffered follow-ups before purging the maps, so a running turn can't deliver a stale response into — or resurrect — the cleared session. - collect-drain failure log now includes the sessionId and last sender so concurrent sessions are diagnosable. - Extract the duplicated sender-name sanitizer into a shared sanitizeSenderName helper used by both ChannelBase and QQChannel; reuse isSharedGroupSession for the /who scope note. - Tests: quote-breakout payload, /clear cancels in-flight, /who in a DM, single-scope hasSession/removeSession, and a sanitizeSenderName unit test. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): make group slash-command pass-through race-free and invalidate queued turns on /clear CRITICAL 1 (sender attribution race): availableCommands is populated asynchronously by the ACP available_commands_update notification, so a registration check could see an empty list on a fresh session and wrongly [sender]-prefix the first real /command — corrupting it into plain text. Stop inline-prefixing ANY slash-shaped message: pass it through verbatim so it always parses as a command regardless of load/registration state. Also widen the command token to include - and : so /compress-fast and /git:commit parse as commands. Tradeoff: an UNREGISTERED group slash command no longer carries [sender] attribution (inverse of the earlier R2 ask) — not breaking real commands is the safer default; flagged for maintainers. CRITICAL 2 (/clear vs queued followups): a teammate turn that entered handleInbound() before /clear confirm had already captured the prev.then() chain and would still run bridge.prompt() against the just-cleared session. Add a per-session generation counter bumped up-front by /clear; a queued turn snapshots it at enqueue and bails if it was bumped before the turn dequeued. Tests: fresh-session/empty-availableCommands pass-through, hyphenated command, unrecognized command (flagged behavior), non-slash still prefixed; /clear genuinely awaits a PENDING in-flight turn before confirming; /clear confirm invalidates an already-queued followup (no resurrection); collect-drain failure logs the lost count with sessionId + sender to stderr. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): only skip group [sender] tag for real slash commands The group attribution check keyed off the lenient parseCommand(), which accepts slash-prefixed paths (e.g. /tmp/foo) as commands, so that prose reached the model without the [sender] tag. Decide command-vs-prose with a dedicated isSlashCommand() that mirrors the CLI's classifier (cli ui/utils/commandUtils.ts isSlashCommand): reject //, /*, a bare /, and any path separator in the first token. Slash-prefixed paths and comments now keep their speaker attribution, while real commands (/compress, /git:commit) still pass through verbatim. Purely lexical, so it stays race-free against the async command list. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): bound /clear's wait on a wedged in-flight turn doClear awaited active.done after cancelSession, but active.done only resolves in the prompt() finally. If the ACP child is wedged (stuck tool call, not reading stdin, or crashed without closing), cancelSession may throw (swallowed) or succeed yet the prompt never returns, so active.done never resolves and /clear — and the whole channel — hangs forever. Race the wait against a CLEAR_CANCEL_TIMEOUT_MS (3s) timeout and purge anyway when it wins: the per-session maps are still purged and the generation already bumped, so a turn that settles later is invalidated. Cancellation stays best-effort. Tests: a wedged session whose active.done never resolves still completes /clear (purges every map, replies "Session cleared") within the timeout via fake timers — no real wait(ms); plus /clear Confirm / CONFIRM (mixed-case) is accepted, guarding the handler's .toLowerCase(). Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): test QQ group slash branch, log dropped queued turns, reclaim cleared generations Address review feedback on the Phase-0 qwen-tag /clear + group-slash work: - QQChannel: cover the previously-untested `isSlash` branch of handleGroup — a group `/clear` is forwarded verbatim (no `[sender]` tag) and must NOT set `alreadyPrefixed`. Catches a regression that always sets it. - ChannelBase: when a followup turn is dequeued after `/clear` bumped the session generation, log the drop (with sessionId) instead of returning silently, so an unanswered queued message is diagnosable. - ChannelBase: reclaim the bumped `sessionGenerations` entry once the cleared session's queue drains (or immediately when nothing was queued), so a long-running gateway no longer leaks one entry per `/clear`. Reclamation is deferred and guarded so it can't delete an entry a still-queued turn needs, and a wedged in-flight turn can't block `/clear`. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): harden qwen-tag group session gating, cancel bounds, and prompt sanitization Address the latest PR #5888 review batch (4 Criticals + suggestions): - Treat sessionScope 'single' as a SHARED group session: the shared-scope predicate now covers both 'thread' and 'single' (via SHARED_SCOPES), so a 'single'-scoped group can no longer bypass /clear confirm + allowlist gating and wipe the channel-wide __single__ session with a bare /clear. - Bound the steer-mode wind-down wait: race active.done against CLEAR_CANCEL_TIMEOUT_MS (and fire-and-forget the cancel) so a wedged ACP child can't pin the session queue forever, matching the /clear hardening. - Make /clear's cancelSession request fire-and-forget so a wedged cancel request can't hang /clear before the bounded active.done wait even starts. - Strip Unicode line/paragraph separators (U+2028/U+2029) and bidi overrides (U+202A-U+202E, U+2066-U+2069) from sender names and quoted text via shared sanitize helpers (sanitizeSenderName, new sanitizeQuotedText); route referencedText and attachment filenames through the shared quoted sanitizer. - Add sender/conversation context to the generation-bail drop log and the DingTalk unroutable-group-message drop log. - Tests: fix TS4111 bracket-access in ChannelBase tests (test-inclusive typecheck clean); add load-bearing coverage for single-scope sharing, wedged steer, wedged /clear cancel, Unicode/bidi sanitization, and the generation-reclamation guard fire paths. - Correct the isSlashCommand JSDoc to document the intentional bare-'/' divergence from the CLI classifier; note SessionRouter's by-sender scan does not match single-scoped keys (latent today). Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): stop block-streaming on /clear, not just cancel it doClear flipped active.cancelled = true but never called active.stopStreaming(), unlike the /cancel handler. In block-streaming channels, text already buffered in the BlockStreamer could still be emitted by the idle timer during/after /clear, leaking a stale response into the just-cleared session. Mirror /cancel: also tear down the streamer on the cancelled prompt in the clear path. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): gate /clear for single-scope DMs and unstick wedged steer queue Two follow-up fixes in ChannelBase, both on my recent fixes. CRITICAL 1: `single` scope maps EVERY sender — group OR DM — to the one `__single__` session, but the shared-session guard also required `isGroup`, so anyone who could DM the bot could bare-/clear the channel-wide session without the confirm + allowedUsers gate. The shared-session predicate now treats `single` as shared regardless of isGroup (`thread` stays group-only); /help and /who follow the same predicate. CRITICAL 2: the bounded steer wait stopped `await active.done` from hanging, but the replacement turn was still chained behind the wedged turn's never-resolving sessionQueues tail, so the follow-up (and every later message) hung forever. On the steer timeout we now re-seed the chain (prev = resolved) so the next turn starts a fresh chain; the wedged turn stays cancelled, so a late settle still can't deliver a stale response. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): sanitize attachment filePath and tighten isSlashCommand The attachment filePath was embedded raw in the prompt while the filename beside it was sanitized. Adapters build the path by basename()-ing the user-supplied filename, so its last segment carries the same attacker-controlled chars (brackets, newlines, U+2028, bidi overrides), letting a crafted filename inject prompt lines via the path. Route the rendered path through the same sanitizeQuotedText neutralization; att.filePath itself is left intact, and benign names render unchanged so the agent's read-file tool still resolves them. Also tighten isSlashCommand to require parseCommand()'s token charset ([a-zA-Z0-9_:-]+ plus an optional @botname). A non-command-shaped input like /cafe or a zero-width-laden token previously skipped the group [sender] tag yet was not a runnable command, reaching the shared session as unattributed prose. It now keeps its attribution. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): guard activePrompt cleanup against replacement-turn clobber The per-turn `finally` in ChannelBase deleted `activePrompts[sessionId]` unconditionally. With the steer-mode bounded wait, this corrupts steer protection: when turn A wedges (its `bridge.prompt()` never resolves), the `CLEAR_CANCEL_TIMEOUT_MS` race times out (`steerWedged`), and turn B starts a fresh chain and re-seeds `activePrompts` with its own entry. When A's prompt finally settles and reaches its `finally`, the unconditional delete removes B's entry — so a later turn C sees `activePrompts.get(sessionId) === undefined` and silently loses steer protection (it forwards verbatim instead of cancelling + re-prompting). Fix: capture this turn's own `ActivePrompt` and compare-and-delete — only clear the entry if it is still ours (`activePrompts.get(sessionId) === promptState`). Sibling-map audit (same later-settling-wedged-turn clobber): - collectBuffers: also gated on the same `stillCurrent` flag. A replaced wedged turn must not drain the buffer the live replacement turn now owns (reachable via mixed-mode single-scope, where a steer turn and collect follow-ups share one session). Behavior-preserving: in all serialized flows the turn is still current at its finally, so the drain runs exactly as before. - sessionQueues / sessionGenerations / instructedSessions: not mutated in the per-turn finally (only in /clear), so no per-turn clobber. Queued turns are already guarded against /clear by the sessionGenerations counter. - promptState.resolve(), bridge.off, streamer.stop, onPromptEnd: per-turn-owned cleanup, kept unconditional (a replaced wedged turn must still release any steer/clear waiter racing its done promise). Adds a deterministic test (manual deferred, no timers/sleeps) reproducing A wedged -> B replaces -> A settles late, asserting B's entry survives and a following turn C still engages steer protection. Reverting the guard fails it. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): guard onPromptEnd on superseded turns and strip control chars from sender names When a steer-replacement turn B re-seeds a session after the bounded wind-down wait times out, a wedged predecessor turn A can settle late and run its finally. onPromptEnd hides a session/chat-scoped working indicator (e.g. the Telegram/WeChat typing indicator keyed by chatId), so A's previously-unconditional onPromptEnd would stop B's indicator while B is still working. Guard onPromptEnd with the same stillCurrent identity check that already protects the per-session map cleanup, so a superseded turn can no longer clear the successor's indicator. A's own teardown (textChunk listener, streamer, promptState.resolve) stays unconditional. Bring sanitizeSenderName to parity with its sibling sanitizeQuotedText by stripping C0/DEL control chars: a crafted display name with \x07/\x1b otherwise reaches the [name] prompt tag. Mirror the same strip in the qqbot send.test.ts mock so a control-char regression in the real helper is caught. Add a diagnostic stderr line when a steer abandons a wedged turn, matching the existing queue-drop log. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): preserve path chars in attachment filePath; trim sender names Addresses ci-bot review on the channel prompt-sanitization paths. filePath over-sanitization (the main regression): a prior hardening pass routed the rendered attachment path through sanitizeQuotedText, which strips the `[`, `]`, and `"` characters. Those are valid, common filesystem path characters (e.g. a Next.js `app/[slug]/page.tsx` route, a quoted segment, or a space in a folder name), so stripping them advertised a `saved to:` path that does not exist on disk and broke the agent's read-file tool. A path rendered alone on its own line cannot use brackets/quotes/spaces to break out of that line, so the only real injection risk is characters that break or reorder the line. Add a path-safe neutralizer, sanitizePromptPath, that strips ONLY C0/DEL controls (incl. CR/LF), the Unicode line/paragraph separators, and bidi overrides (the PROMPT_UNSAFE_INVISIBLES set), preserving everything else byte-intact and without capping length. The human-readable fileName label keeps sanitizeQuotedText (bracket-stripping is fine for a quoted label). steer generation-capture ordering: in steer mode the session generation was snapshotted AFTER the bounded wind-down wait. A concurrent /clear (e.g. another sender's clear-confirm on a shared session) that bumps the generation DURING that wait was therefore invisible: the snapshot read the post-clear value, the dequeue equality guard still held, and the turn ran against the just-cleared session. Capture preSteerGeneration BEFORE the wait and use it for the guard so a /clear during the wait is detected and the turn bails. sender-name fallback: sanitizeSenderName now trims after the length cap and returns an 'unknown' default, so a name made entirely of strippable chars (brackets/newlines) no longer renders an anonymous bracket tag. Both call sites embed the result as a bracket tag with no fallback of their own, so the default lives in the shared helper; the qqbot test mock mirrors the new contract. Adds load-bearing, mutation-checked tests for each change (deterministic, fake-timer driven for the steer/clear race; no real-time waits). Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): stop streamer on steer-cancel and align slash-command trim Two ci-bot review findings on the steer/slash-command paths in ChannelBase. 1. steer-cancel must stop the BlockStreamer, not just cancel. The steer handler flipped active.cancelled but never called stopStreaming(), unlike the sibling doClear path. cancelled alone only suppresses NEW chunks: text already buffered in a wedged turn's streamer is still flushed by the idle timer (~1500ms), which fires before the 3000ms steer wind-down bound, so it leaks into the chat after the replacement turn has begun. Mirror doClear and stop the streamer immediately after cancelling. 2. parseCommand now trims its input so it agrees with isSlashCommand (which already trims). Before, " /help" (leading space, common from IME/copy-paste) made isSlashCommand return true — suppressing the group [sender] tag — while parseCommand returned null, so the command reached the agent unattributed. Trimming closes that attribution gap; it changes nothing else (the no-whitespace path is unaffected, and /tmp/foo still has no handler). Also document the forward-looking late-cancel concern at the steer cancel site: cancelSession is keyed only by sessionId and the replacement prompt reuses it. The shipping AcpBridge sends cancel+prompt over one in-order stdin stream with cancel enqueued first, so the child always processes the cancel before the new prompt — the replacement turn is safe today. A future network/daemon bridge with cancel latency could reorder them; the proper fix is turn-scoped cancellation (a Bridge-contract change), deferred to avoid an API break here. Tests: a steered wedged turn with buffered streamer text invokes stopStreaming so the idle-timer flush can't deliver stale text after the replacement turn begins; " /help" in a group is handled as a command (no [sender] tag, no forward) while /help and /git:commit still parse. Both are mutation-checked. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): block shell commands in shared sessions; dedupe cancel path Phase 0 ships shared/group sessions (sessionScope 'thread' and 'single') but has no per-sender trust model — the [sender] marker is explicitly not a trust boundary. The pre-existing bang (`!`) handler in ChannelBase runs arbitrary host shell commands, so in a shared session ANY participant could run `!rm -rf /`. Gate it with the same isSharedSession predicate used for the destructive-/clear confirm gate: refuse with a one-line notice in shared sessions, keep direct execution in a 1:1 session (the lone user is the operator). isSharedSession is promoted from a closure to a private method so both gates share one source of truth. Extract the duplicated cancel + bounded-wait sequence from doClear and the steer branch into a private cancelAndAwaitActive() helper (the duplication previously caused steer to miss stopStreaming()). steer keeps its pre-wait generation snapshot and uses the boolean result; doClear ignores it. Also: trim the over-verbose steer/clear comment blocks to the load-bearing why; cap sanitizePromptPath at 1024 chars (defense-in-depth) like its siblings; and make the qqbot send.test mock use the real sanitizeSenderName via vi.importActual instead of an inline re-implementation that can drift. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): run onPromptEnd after /clear; only skip it for superseded turns The per-turn `finally` guarded `onPromptEnd` behind `stillCurrent` alone. When `/clear` cancels an in-flight turn with NO replacement it deletes that turn's `activePrompts` entry, so if the cancelled prompt settles late `stillCurrent` is false and `onPromptEnd` was skipped — leaking the platform cleanup several adapters do there (Telegram clears its typing interval, DingTalk recalls the working reaction, Weixin clears typing, Feishu finalizes card state). The `stillCurrent`-only guard was only meant to stop a STEER-superseded turn from clobbering its replacement's indicator, but it wrongly also caught the no-replacement `/clear` case. Distinguish the two: add `ActivePrompt.superseded`, set on the OLD turn only when a steer replacement actually takes over the session slot. The `finally` now runs `onPromptEnd` UNLESS the turn was superseded; the `activePrompts.delete` and the collect-drain stay `stillCurrent`-gated (a superseded turn must not drain the replacement's buffer). Regression + collect-drain tests added with mutation checks. Also addresses review nits: extract a shared command-token regex constant so parseCommand and isSlashCommand can't drift; report `single` scope as "shared channel-wide" in /who (it is shared across all DMs and groups, not just one group); append a truncation ellipsis in sanitizeQuotedText so a cut quote/filename is detectable; and include the message text in the generation-bail drop log so an ignored message is diagnosable. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): require the slash-command token to immediately follow the slash isSlashCommand used trimmed.slice(1).trimStart() before taking the first token, so a space after the slash (`/ foo`) still classified as a command. parseCommand's regex requires the token immediately after `/`, so it returned null for the same input. In a shared group session that divergence suppressed the [sender] attribution (isSlashCommand true) while running no command (parseCommand null), letting `/ foo` reach the agent unattributed — the exact failure the file's comment warns about. Remove the .trimStart() so the token must immediately follow the slash; `/ foo` now splits to an empty first token and is treated as prose, agreeing with parseCommand. Normal commands (/help, /git:commit, /compress-fast, /cmd@bot) are unaffected. Adds an invariant test that isSlashCommand and parseCommand agree on `/ foo` (both false) and /help (both true), plus a behavioral test that `/ foo` keeps its [sender] tag. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): don't clobber a post-/clear turn's indicator; tidy refusal + diagnostics Addresses wenshao's review on PR #5888: - Refusal text: `!` shell commands are "disabled in shared sessions" (drop "group" — isSharedSession is also true for single-scope DMs, which are not groups). - /clear diagnostic: capture cancelAndAwaitActive's result and log on timeout, mirroring the steer "abandoned a wedged turn" message, so a wedged /clear is observable instead of silently "worked". - /clear indicator clobber: a /clear that evicts a wedged turn never set `superseded`, so the turn's late-settling finally still ran onPromptEnd and killed the indicator a turn started AFTER the /clear now owns. Add a `clearEvicted` flag: /clear runs the wedged turn's own onPromptEnd at eviction time (no replacement exists yet) and marks it clearEvicted; the late finally then skips onPromptEnd. onPromptEnd now fires exactly once for the evicted turn, at clear-time. The finally guard becomes `stillCurrent || (!superseded && !clearEvicted)`. ActivePrompt carries the originating chatId/messageId so the eviction can target the right indicator. - Dropped-queued-turn log: sanitize the attacker-controlled message text (render newline visibly, strip C0/DEL incl. CR/ESC) before it reaches an operator's terminal, matching every other embed path. Tests: complete the single-scope DM `!` refusal test (not-called + length + not-forwarded); update the group/DM/1:1 refusal assertions to the new text; re-point the "/clear settles late" test to clear-time cleanup; add a replacement-after-/clear test asserting a late settle does not end a later turn's indicator; strengthen the dropped-turn-log test with control-char input. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): fix root tsc errors in channel tests; gate /who; sanitize dingtalk nick log Root `tsc --noEmit` failed on 9 type errors in two channel test files that per-package builds and `--workspace` typechecks miss (both exclude test files). Fix the test types only — no production type changes: - qqbot/dingtalk: `QQChannel`/`DingtalkChannel` come from `await import()` as values, so annotate instances with `InstanceType<typeof X>`. - dingtalk test config was missing the required `token` field of `ChannelConfig`. - qqbot mock: bracket-access the index-signature `router` and type the assignment so `{}` is assignable to `Record<string, unknown>`. /who: gate it to authorized senders in shared sessions, mirroring /clear — /who leaks the workspace basename, so non-members shouldn't see it. dingtalk: sanitize `senderNick` with the shared `sanitizeSenderName` before writing it to stderr, so a crafted nick with CR/LF/control chars can't fragment or inject log lines. !-shell gate: refuse `!` shell commands in ALL groups (gate on `envelope.isGroup`, not just shared sessions) — a user-scope group is not a shared session yet is still multi-operator, so members could reach the host shell. The refusal now runs BEFORE router.resolve, so a refused command never creates a session. Single-scope DMs stay refused. tests: the qqbot/dingtalk suites mock `@qwen-code/channel-base` and pull the real `sanitizeSenderName` via the package export, which resolves to base/dist and broke clean package-local runs. Alias the specifier to base's source in each vitest config (mirrors cli) so the suites run without a prior `tsc --build` of base. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): bump generation and release indicator on steer-abandon, mirroring /clear When a steer abandons a wedged turn it re-seeds a fresh queue chain but, unlike doClear, did neither of the two protections doClear runs on a wedged-turn eviction. Both bugs surface when mixed dispatch modes collapse onto one session (single/user scope). FIX 1 (generation bump): a followup queued behind the wedged turn stays on the now-orphaned chain. Without bumping sessionGenerations, when the wedged turn late-settles the followup PASSES the dequeue guard and runs the unguarded activePrompts.set, clobbering the live steer replacement -> two concurrent bridge.prompts on one session (duplicated responses + double tool execution). Bump the generation up-front (skipping if a /clear raced the wind-down wait) and advance the replacement's captured snapshot so it proceeds while the orphaned followup bails. FIX 2 (messageId-scoped indicator leak): the supersede path never released the abandoned turn's OWN indicator. A CHAT-scoped indicator is re-seeded by the replacement, but a MESSAGEID-scoped one (per-message reaction/card keyed on the inbound messageId) is keyed on the abandoned turn's messageId and leaks until disconnect. Run the abandoned turn's onPromptEnd at steer-time and mark it superseded so its late finally still skips onPromptEnd (released once, no double-fire, replacement untouched). Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): steer waits for the running turn instead of racing a concurrent replacement In steer dispatch, when a new message arrived while a prompt was running, ChannelBase cancelled the running turn (best-effort), did a BOUNDED wait, and on TIMEOUT (the old turn wedged, not finished) PROCEEDED to start a replacement bridge.prompt() on the SAME sessionId while the old prompt was still active. As wenshao flagged, this is bridge-unsafe: - DaemonChannelBridge.prompt() REJECTS while the prior prompt is still marked active, so the replacement is silently dropped. - Both bridges collect/emit chunks keyed by sessionId ONLY, so the abandoned turn's late chunks mix into the replacement's stream (duplicated/stale output). Fix (wenshao option (a)): steer now best-effort cancels the running turn and CHAINS the new turn onto the existing session queue tail, so it runs only AFTER the old turn's finally has actually run (onChunk detached, activePrompts cleared, indicator released). The cancel stays — it makes the old turn wind down sooner — but steer never proceeds while a turn is still active, eliminating the unsafe concurrency entirely. Steer now differs from followup only by that best-effort cancel. Removed the steer-only concurrency machinery that only existed to host a concurrent replacement: the steer-path bounded wait (cancelAndAwaitActive call), the steerWedged flag and fresh-chain re-seed, the preSteerGeneration capture, the steer-time sessionGenerations bump, the steer-time onPromptEnd, and the superseded flag (plus its finally guard term). /clear's OWN protections — its up-front generation bump, eviction-time onPromptEnd, and the clearEvicted finally guard — are SEPARATE and preserved; /clear genuinely evicts and still needs them. cancelAndAwaitActive is retained, now used only by /clear. BEHAVIORAL CHANGE: steer no longer force-interrupts a genuinely wedged turn; it cancels it and waits for it to finish before the new turn runs. Turn-scoped cancellation/routing (so a new turn can run without waiting for a wedged predecessor) is wenshao option (b) — it needs an API change across every adapter and is the deferred enhancement, out of scope here. Folded in two further #5888 review items: - [Critical] QQ slash-command audit-log injection (qqbot/src/QQChannel.ts): the audit process.stderr.write interpolated the RAW, attacker-controlled senderName (event.author.username) and cleanText BEFORE sanitization, so a crafted nick or message with CR/LF/ANSI escapes could forge or corrupt operator audit logs. Hoisted `safeName = sanitizeSenderName(senderName)` above the audit log and now log a neutralized command string (cap 80, render \n visibly, strip C0/DEL), mirroring ChannelBase's dropped-turn log and the DingTalk hardening already in this PR. The prompt-path sanitizeSenderName usage is unchanged. - [Suggestion] single-scope DMs were not attributed (base/src/ChannelBase.ts): the [sender] prefix was gated on envelope.isGroup alone, but sessionScope:'single' collapses every sender's DM into one __single__ session (already treated as shared by the !-gate, /clear confirm and /who), so different people merged into one unattributed conversation (the RFC-R4 gap Phase 0 closes). The gate is now (envelope.isGroup || sessionScope === 'single') — deliberately NOT isSharedSession, which is false for user-scope groups that must keep attribution. Tests: removed the obsolete steer-concurrency cases and added cases for the new steer behavior (new turn starts only after the old completes; the abandoned turn's late chunks cannot reach the new turn). Added an audit-log sanitization test (QQ) and single-scope-DM / user-scope-group / 1:1-DM attribution tests (base). /clear's eviction protections are unchanged and still pass. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): neutralize NEL/C1 in prompt text; dedupe shared-session auth gate Address wenshao's review on the Phase-0 qwen-tag channel base. - sanitize: PROMPT_UNSAFE_INVISIBLES now also neutralizes the C1 control block (U+0080-U+009F), which includes NEL (U+0085), a Unicode line break (UAX#14 BK) that renders as a new line. Without it a display name like "Alice<NEL>system: ..." or a crafted reply quote could inject a prompt line. Applies to both sanitizeSenderName and sanitizeQuotedText (and the shared path sanitizer) since they share the set. Tests cover NEL + a second C1 (CSI U+009B) for sender names and quoted text; mutation-checked. - ChannelBase: the two best-effort cancelSession() calls (the /clear wind-down wait and the steer pre-cancel) no longer swallow the IPC failure with an empty catch. They now log channel name + sessionId + reason to stderr, matching the existing /cancel log style, so a wedged turn is diagnosable. Still fire-and-forget (not awaited). - ChannelBase: extract the verbatim shared-session authorization gate shared by /clear and /who into a private isAuthorizedForSharedSession() predicate (isSharedSession + allowedUsers check). Behavior is unchanged; each caller keeps its own rejection wording. - tests: add a GROUP-path case that /help@mybot is treated as a command (no [sender] prefix, not forwarded to the agent); the existing @botname test only covered a DM. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): strip C1/NEL from audit-log text too, matching the prompt path The prompt path now neutralizes the C1 control block (U+0080-U+009F, incl. NEL U+0085, a Unicode line break) via PROMPT_UNSAFE_INVISIBLES, but the inline stderr audit-log sanitizers still stripped only C0/DEL. A crafted message carrying NEL/C1 therefore survived into the dropped-/queued-turn log (ChannelBase) and the QQ slash-command audit log, where many terminals render NEL as a newline, forging an extra [channel] log line. Extend both inline strips to also cover the C1 block (C0 + DEL + C1), matching the prompt path's union, and refresh the now-stale comments. Tests feed a NEL (U+0085) and a C1 char (U+009B) through both log paths and assert neither survives; reverting the strip to C0/DEL fails the new NEL assertions. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): gate /status and /cancel in shared sessions; code-point-safe truncation Three follow-ups to the shared-session authorization work: - /status: add the same isAuthorizedForSharedSession gate /who uses, so a non-member of a shared session with a non-empty allowedUsers list can no longer read its session/access state. Non-shared (DM/per-user) is unchanged. - /cancel: gate the destructive abort behind isAuthorizedForSharedSession like /clear (auth gate only, no confirm), so a group member can't cancel another user's in-flight turn on a shared session. 1:1 and authorized users unchanged. - sanitize: truncate sanitizeSenderName/sanitizeQuotedText/sanitizePromptPath on Unicode code-point boundaries (Array.from) instead of UTF-16 code units, so a cap landing mid-surrogate-pair (e.g. an emoji) can't leave a lone surrogate that renders as the replacement character downstream. The ellipsis logic in sanitizeQuotedText still keeps the result within maxLen code points. Adds tests for each (incl. mutation-checked gates and an emoji-at-cap case). Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): audit-log shared /clear and blocked ! shell; test steer stopStreaming Address ci-bot review on PR #5888: - [Observability] Emit a stderr audit line on a successful clear of a SHARED session (channel, sessionId, sanitized sender + stable senderId). A 1:1 DM clear is single-participant and is not logged. - [Observability] Emit a stderr audit line when a group/shared member's `!` host-shell command is refused, so operators can detect blocked attempts. Sender name is sanitized; the command payload is not echoed. - [Test gap] Add a steer test asserting the best-effort cancel calls active.stopStreaming (spying on the running turn's prompt), plus tests for both new audit lines and the DM no-log case. The /cancel and /status shared-session auth gates flagged by ci-bot were already present on this branch and are left unchanged. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): gate steer-cancel by authorization; close audit-log + attribution injection gaps Address review on PR #5888 (qwen-tag-phase0): - [Critical] The /cancel auth gate was bypassable via the default `steer` dispatch mode: any normal message cancels the running turn, so an unauthorized member of a shared session - blocked from /cancel - could abort another user's active turn just by sending a message. The steer branch now checks isAuthorizedForSharedSession FIRST and, when unauthorized, breaks to normal queuing (the message chains onto the session queue tail and runs AFTER the active turn instead of aborting it). - [Suggestion] Audit-log sanitizers missed U+2028/U+2029 and the bidi overrides (log-line spoofing / trojan-source). Exported PROMPT_UNSAFE_INVISIBLES and added a shared sanitizeLogText(text, maxLen) helper that applies BOTH that set AND the C0/DEL strip (and caps length), used at both audit-log sites (ChannelBase dropped-turn log + QQ slash-command audit log) so the defense can't drift apart. - [Suggestion] The [sender] attribution prefix was suppressed for any command-SHAPED text, so unrecognized "/x\n[SYSTEM]: ..." reached the agent unattributed. Added a synchronous isRecognizedCommand() (locally registered commands + the bridge.availableCommands snapshot) and now suppress the prefix only when the text is BOTH a command shape AND a recognized command; unrecognized command-like text keeps its [sender] tag. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): recognize command aliases per-session so attribution doesn't break them isRecognizedCommand decides whether to suppress the group [sender] attribution prefix, but matched only against availableCommands[].name. The ACP parser also accepts command ALIASES (parseSlashCommand altNames), so a valid alias like /summarize (alias of /compress) or /login (alias of /auth) was classified as unrecognized, rewritten to "[Alice] /summarize", and then run as PLAIN CHAT (the leading / is gone) instead of executing. It also read the bridge's GLOBAL availableCommands snapshot, which in DaemonChannelBridge can belong to another session. - Carry aliases through the available-command contract: add an optional altNames?: string[] to AvailableCommand, emit them on the wire in _meta.altNames (ACP's extension point; a top-level altNames would be an excess-property error against the SDK type), and lift them in both bridges via readAvailableCommandAltNames. Omitted when absent, so alias-free entries stay byte-identical. - Match name AND altNames, against THIS session's command list: isRecognizedCommand now takes sessionId and reads getAvailableCommands(sessionId) when the bridge exposes it (DaemonChannelBridge), falling back to the global getter (AcpBridge, single-agent, inherently session-correct). Stays synchronous: a real command sent before the snapshot loads keeps its tag (safe default). - Security intent intact: genuinely-unrecognized command-shaped text (e.g. /x\n[SYSTEM]: ...) still keeps its [sender] tag. Also fold in two #5888 review items in ChannelBase.ts: - Steer auth gate: audit the silent steer->queue downgrade to stderr for an unauthorized member (operator-visible only; no per-message reply), matching the /cancel,/clear,/who,/status gates' observability. - /clear eviction: a clear-time onPromptEnd that throws would abort the purge, leaving the evicted turn in activePrompts so its late finally (stillCurrent || !clearEvicted) re-runs onPromptEnd and clobbers a newer turn. Set clearEvicted first and catch+audit the throw so the purge always runs (turn becomes non-current) and the late finally skips. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): match agent commands case-sensitively; add steer wedge watchdog isRecognizedCommand lowercased the token before comparing it to agent command names/aliases, but the CLI's parseSlashCommand matches agent commands CASE-SENSITIVELY (`cmd.name === part`, `cmd.altNames?.includes(part)`). So a wrong-case token like `/SUMMARIZE` was "recognized" here — suppressing the `[sender]` attribution tag — yet ran NO command in ACP, which then forwarded the raw text unattributed, reopening the injection where a crafted `/SUMMARIZE\n[SYSTEM]: …` second line reaches a shared group as an apparent system directive. parseCommand now also returns the typed-case `raw` token; isRecognizedCommand matches AGENT commands on `raw` (case-sensitive, mirroring the CLI) while LOCAL commands keep their existing case-INSENSITIVE match (registerCommand lowercases the stored name; handleInbound dispatches by the lowercased token). The steer chain-and-wait path lost the old "abandoned wedged turn" log, so a hung predecessor bridge.prompt() could silently deadlock the session with no observability. Add a diagnostic-only watchdog: the steer branch arms a timer (unref'd) that, if the predecessor is still the active prompt after CLEAR_CANCEL_TIMEOUT_MS, emits a stderr line pointing at /clear for recovery; the chained `.then()` disarms it as its first statement once the predecessor's tail resolves. No concurrency change — chain-and-wait is untouched. Also simplify the late-finally onPromptEnd guard from `stillCurrent || !clearEvicted` to `!clearEvicted`: clearEvicted is set ONLY by /clear's eviction, which then unconditionally deletes activePrompts (its try/catch around the clear-time onPromptEnd guarantees the purge even if it throws) and never re-inserts the same promptState, so `clearEvicted` implies `!stillCurrent` and the dropped term was unreachable. Tests: wrong-case `/SUMMARIZE` keeps `[sender]` (with a mutation note); wrong- case local `/HELP` still dispatches locally; fake-timer watchdog tests for the wedged (logs) and settled (timer cleared, no log) paths. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): align agent command recognition with parseSlashCommand; guard finally onPromptEnd Address maintainer review on the qwen-tag Phase 0 channel adapter. - [Critical] Guard the normal-completion onPromptEnd in the per-turn finally. onPromptEnd runs platform-adapter cleanup (typing interval, working-reaction recall, card finalize) - network/IO that can throw. An uncaught throw skipped activePrompts.delete (session leak), promptState.resolve (active.done never settled, so a later /clear falsely logged "abandoned a wedged turn"), and the collect-buffer drain - and the rejection, swallowed by the queue tail's .catch(() => {}), silently dropped every later turn. Wrap it in try/catch with a stderr log, matching the eviction-path treatment. - [Critical] End the channel/agent command-recognition divergence by matching the AGENT branch of isRecognizedCommand EXACTLY as the CLI's parseSlashCommand does: the FIRST whitespace token after the leading '/', case-SENSITIVELY, WITHOUT stripping an @suffix. parseCommand's '@'-stripped, lowercased token diverged from the agent (PARSE_COMMAND_RE drops '(?:@\S+)?'), so /compress@x or /Compress were "recognized" here (tag suppressed) yet ran no command there and reached the model unattributed. Matching the exact token leaves wrong-case / @suffix / injection- shaped tokens UNRECOGNIZED -> they keep their [sender] tag (attributed), exactly as the agent treats them. No prompt rewrite: a /compress@otherbot aimed at another bot must not run here. LOCAL commands keep case-insensitive dispatch. - [Suggestion] Include the originating chatId/messageId (sanitized) in the "/clear abandoned a wedged turn" log so oncall can correlate the stuck turn. - [Suggestion] Clarify the alreadyPrefixed JSDoc: it is also set by the QQ adapter on real self-prefixed inbounds, which sanitize the embedded name at the source - so the flag does not bypass sanitization (verified; no behavior change needed). - [Suggestion] Replace the blind `as unknown as {...}` bridge cast on the recognition path with a typed AgentCommandsProvider interface (optional members), so a future rename/return-type change is type-checked instead of breaking at runtime. - [Suggestion] Validate altNames' shape: isAvailableCommand now rejects a non-array altNames, and the recognition site guards the alias check with Array.isArray, so a malformed wire payload can't throw at the `.includes` call. Tests: throwing-onPromptEnd cleanup + collect-drain + log; exact-token recognition (verbatim /compress and /summarize alias; tag kept for /SUMMARIZE, /COMPRESS, /compress@x, and /compress@x + [SYSTEM] line); wedged-turn log carries chat/message; malformed altNames does not throw; isAvailableCommand drops a malformed-altNames entry. Restored stderr spies in the two steer-watchdog tests' finally. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(channels): address qwen tag review followups --------- Co-authored-by: Qwen-Coder <noreply@qwen.ai> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
8b65a555a0
|
chore(release): v0.19.3 (#5952)
* chore(release): v0.19.3 * docs(changelog): sync for v0.19.3 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
714513df20
|
feat(channels): register Telegram bot command menu (#5919) | ||
|
|
5bb79bc67c
|
chore(release): v0.19.2 (#5830)
* chore(release): v0.19.2 * docs(changelog): sync for v0.19.2 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
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> |
||
|
|
8eb5770812
|
chore(release): v0.19.1 [skip ci]
* chore(release): v0.19.1 * docs(changelog): sync for v0.19.1 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
57156522bd
|
chore(release): v0.19.0 [skip ci]
* chore(release): v0.19.0 * docs(changelog): sync for v0.19.0 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
8f8ed0d7c1
|
chore(release): v0.18.5 [skip ci]
* chore(release): v0.18.5 * docs(changelog): sync for v0.18.5 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
c5fb75b5c2
|
chore(release): v0.18.4 [skip ci]
* chore(release): v0.18.4 * docs(changelog): sync for v0.18.4 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
295860db24
|
fix(telegram): clear typing intervals on disconnect (#5477) | ||
|
|
fc15ae6cb7
|
fix(dingtalk): skip uppercase webhook reaction targets (#5466) | ||
|
|
5924ab3a95
|
fix(weixin): handle uppercase CDN upload schemes (#5439) | ||
|
|
409a612fec
|
fix(channel): bound qqbot gateway reconnect retries (#5415) | ||
|
|
617eb20081
|
fix(channel): keep qqbot token refresh retrying (#5414) | ||
|
|
30bfe63528
|
fix(channel): track qqbot close reconnect timer (#5416) | ||
|
|
8477560b8d
|
fix(channel): scope qqbot session backup path (#5417) | ||
|
|
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 |
||
|
|
5a84ba016b
|
fix(weixin): confirm the WEBP signature, not just the RIFF prefix (#5285)
detectImageMime treated any data starting with the four RIFF bytes as image/webp, but RIFF is a generic container also used by WAV and AVI. A non-WebP RIFF file (e.g. a WAV renamed to .webp) therefore passed the magic-byte check in validateImagePath, which exists to confirm a file's real type. Verify the 'WEBP' marker at bytes 8-11 as well, matching the stricter check already in core's imageTokenizer and the earlier full-signature PNG fix. |
||
|
|
a45b14fba3
|
fix(weixin): normalize markdown image syntax (#5297) | ||
|
|
06fdd59cf2
|
fix(dingtalk): split oversized markdown lines (#5299) | ||
|
|
0a0fedf984
|
fix(weixin): show allowed image directories (#5296) | ||
|
|
45220c219c
|
chore(release): v0.18.3 [skip ci]
* chore(release): v0.18.3 * docs(changelog): sync for v0.18.3 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |