Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
de769d5
feat(channels): add WeCom smart bot channel
qqqys Jul 3, 2026
f32f098
fix(channels): harden wecom review suggestions
qqqys Jul 3, 2026
b2dbd5e
fix(channels): address wecom critical review
qqqys Jul 3, 2026
6100fd1
fix(channels): include wecom mixed voice text
qqqys Jul 3, 2026
1f86d11
fix(channels): tighten wecom outbound media
qqqys Jul 3, 2026
30076d4
fix(channels): harden wecom outbound sends
qqqys Jul 3, 2026
067d74c
fix(channels): address wecom review blockers
qqqys Jul 3, 2026
74563a1
fix(channels): address wecom review followups
qqqys Jul 3, 2026
3c14ef9
fix(channels): harden wecom inbound handling
qqqys Jul 3, 2026
ccd4f33
fix(channels): address wecom auth and media review
qqqys Jul 3, 2026
4588045
fix(channels): tighten wecom inbound cleanup
qqqys Jul 3, 2026
5a2abaf
fix(channels): harden wecom media safety
qqqys Jul 3, 2026
547f67f
fix(channels): address wecom review typecheck
qqqys Jul 3, 2026
46a2efc
fix(channels): harden wecom media review gaps
qqqys Jul 3, 2026
6c66094
fix(channels): address wecom review blockers
qqqys Jul 3, 2026
9ccf0c9
fix(channels): tighten wecom media edge cases
qqqys Jul 3, 2026
006023d
fix(channels): address wecom review blockers
qqqys Jul 3, 2026
645406b
fix(channels): address wecom media review blockers
qqqys Jul 4, 2026
e6a0e3a
fix(channels): address wecom review follow-ups
qqqys Jul 4, 2026
36ea9f9
fix(channels): address wecom review blockers
qqqys Jul 4, 2026
3e923e1
fix(channels): close wecom review blockers
qqqys Jul 4, 2026
bc95cb9
fix(channels): close wecom preflight dedup race
qqqys Jul 4, 2026
92d129c
fix(channels): close wecom review gaps
qqqys Jul 4, 2026
7016ea5
fix(channels): harden wecom kick reconnect
qqqys Jul 4, 2026
5ad0c9c
fix(channels): defer wecom session resolution
qqqys Jul 4, 2026
643555d
fix(channels): clean wecom session attachments
qqqys Jul 4, 2026
5a9c9b0
fix(channels): harden wecom reconnect and media cleanup
qqqys Jul 4, 2026
7947aff
fix(channels): address wecom review diagnostics
qqqys Jul 4, 2026
7dea3a7
fix(channels): improve wecom diagnostics
qqqys Jul 4, 2026
9ba9cc9
fix(channels): reset wecom kick retries
qqqys Jul 4, 2026
8ab3343
fix(channels): improve wecom diagnostics
qqqys Jul 4, 2026
05936c1
fix(channels): preserve sync cancel preflight
qqqys Jul 4, 2026
d9a9b84
fix(channels): close wecom connection and ssrf gaps
qqqys Jul 4, 2026
6e25684
fix(channels): clean coalesced wecom attachments
qqqys Jul 4, 2026
c3350a1
fix(channels): bound wecom sdk connect wait
qqqys Jul 4, 2026
f49032b
Merge branch 'main' into codex/issue-6208-wecom-channel
wenshao Jul 4, 2026
40ca360
fix(channels): scope wecom untracked attachment cleanup
qqqys Jul 5, 2026
ebb6e63
fix(channels): block wecom nat64 local-use ssrf
qqqys Jul 5, 2026
08dbc93
fix(channels): harden wecom media handling
qqqys Jul 5, 2026
f58a17d
fix(channels): harden wecom group gates
qqqys Jul 5, 2026
d9bb953
fix(channels): bound wecom kick reconnect cycles
qqqys Jul 5, 2026
f091c36
fix(channels): drain loop collect prompts directly
qqqys Jul 5, 2026
907c232
fix(channels): align wecom buffer hooks
qqqys Jul 5, 2026
8bc756d
fix(channels): harden wecom delivery failures
qqqys Jul 5, 2026
003c985
fix(channels): recover from wecom attachment write failures
qqqys Jul 5, 2026
3ba8e95
fix(channels): surface wecom media send failures
qqqys Jul 5, 2026
f03c008
fix(channels): harden wecom replay and reconnect
qqqys Jul 5, 2026
f1998d1
fix(channels): clarify wecom partial delivery cleanup
qqqys Jul 5, 2026
8a4b6bd
fix(channels): close wecom rejected downloads
qqqys Jul 5, 2026
2bfe8b3
fix(channels): retain wecom dedup after processing starts
qqqys Jul 5, 2026
dcd4170
fix(channels): harden wecom reconnect and media errors
qqqys Jul 5, 2026
8f56b3f
fix(channels): add wecom media error context
qqqys Jul 5, 2026
81d6d1c
fix(channels): improve wecom dns diagnostics
qqqys Jul 5, 2026
c988e15
fix(channels): keep wecom kick retry alive
qqqys Jul 5, 2026
951c1b5
fix(channels): allow wecom quoted bot replies
qqqys Jul 5, 2026
bfaf08f
fix(channels): preserve wecom code fences across chunks
qqqys Jul 5, 2026
3dbcc91
fix(channels): harden wecom reconnect lifecycle
qqqys Jul 5, 2026
0249240
fix(channels): report wecom media dir setup failures
qqqys Jul 5, 2026
60d191b
fix(channels): harden wecom reconnect recovery
qqqys Jul 5, 2026
6e7fab1
fix(channels): align wecom review fixes
qqqys Jul 6, 2026
7661af0
fix(channels): harden wecom marker parsing
qqqys Jul 6, 2026
2380b64
fix(channels): keep wecom reconnect timers alive
qqqys Jul 6, 2026
e2da90a
fix(channels): handle wecom tilde fences
qqqys Jul 6, 2026
0fa9f03
fix(channels): preserve wecom fence state
qqqys Jul 6, 2026
e0ae776
fix(channels): clean up wecom attachment races
qqqys Jul 6, 2026
b650c4c
fix(channels): bind wecom media reads to file handles
qqqys Jul 6, 2026
0fda2a8
fix(channels): prevent wecom symlink media opens
qqqys Jul 6, 2026
4ada37e
fix(channels): address wecom review blockers
qqqys Jul 6, 2026
7d634af
fix(wecom): remove media URL from error messages to prevent credentia…
qqqys Jul 6, 2026
9b5a3cd
fix(wecom): address review feedback — tests, security, correctness
qqqys Jul 6, 2026
5f4b2ab
fix(wecom): wrap client.disconnect() in catch block to preserve conne…
qqqys Jul 6, 2026
8d7c1b6
fix(channels): address wecom reconnect review blockers
qqqys Jul 7, 2026
75bf5b9
fix(channels): harden wecom reconnect review fixes
qqqys Jul 7, 2026
71fc860
fix(channels): harden wecom review blockers
qqqys Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/users/features/channels/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export default {
telegram: 'Telegram',
weixin: 'WeChat',
dingtalk: 'DingTalk',
wecom: 'WeCom',
feishu: 'Feishu',
qqbot: 'QQ Bot',
plugins: 'Plugins',
Expand Down
111 changes: 111 additions & 0 deletions docs/users/features/channels/wecom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# WeCom (Enterprise WeChat)

This guide covers setting up Qwen Code with a WeCom intelligent robot (企业微信智能机器人).

## Prerequisites

- A WeCom organization account
- A WeCom intelligent robot created in API mode
- The robot's Bot ID and Secret

## Creating the Robot

1. Open the WeCom admin console and create an intelligent robot.
2. Choose API mode.
3. Copy the Bot ID and Secret.
4. Add the robot to the direct chats or groups where it should be available.

The intelligent robot uses a WebSocket connection from Qwen Code to WeCom. You do not need a public callback URL, Token, EncodingAESKey, Corp ID, or Agent ID.

## Configuration

Add the channel to `~/.qwen/settings.json`:

```json
{
"channels": {
"my-wecom": {
"type": "wecom",
"botId": "$WECOM_BOT_ID",
"secret": "$WECOM_SECRET",
"senderPolicy": "allowlist",
"allowedUsers": ["zhangsan"],
"sessionScope": "user",
"cwd": "/path/to/your/project",
"instructions": "You are a concise coding assistant responding via WeCom.",
"groupPolicy": "open",
"groups": {
"*": { "requireMention": true }
}
}
}
}
```

Set the credentials as environment variables:

```bash
export WECOM_BOT_ID=<your-bot-id>
export WECOM_SECRET=<your-secret>
```

Or define them in the `env` section of `settings.json`:

```json
{
"env": {
"WECOM_BOT_ID": "your-bot-id",
"WECOM_SECRET": "your-secret"
}
}
```

## Running

```bash
qwen channel start my-wecom
```

Open WeCom and send a message to the intelligent robot.

## Access Control

`senderPolicy` works the same way as other IM channels:

- `allowlist`: only users in `allowedUsers` can use the bot. This is the recommended enterprise default.
- `pairing`: users must pair before using the bot.
- `open`: anyone who can message the robot can use it.

For groups, set `groupPolicy` to `"allowlist"` or `"open"`. By default, group messages require a mention through `"requireMention": true`.

When the WeCom SDK includes explicit mention metadata, Qwen Code uses it for this gate. If no mention metadata is present, the channel treats delivered group messages as mentioned, matching WeCom intelligent robot API mode where group delivery is expected to be scoped by WeCom.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This sentence is now inverted relative to the adapter behavior. WeComAdapter sets isMentioned to false for group messages when no explicit mention metadata is present, and the tests pin that missing metadata is dropped by the default mention gate. As written, users will expect those messages to reach the bot when they will be ignored unless they disable requireMention.

Suggested change
When the WeCom SDK includes explicit mention metadata, Qwen Code uses it for this gate. If no mention metadata is present, the channel treats delivered group messages as mentioned, matching WeCom intelligent robot API mode where group delivery is expected to be scoped by WeCom.
When the WeCom SDK includes explicit mention metadata, Qwen Code uses it for this gate. If no mention metadata is present, the channel treats delivered group messages as unmentioned. Set `"requireMention": false` only if you want to rely on WeCom-side delivery scoping instead.

— GPT-5 Codex via Qwen Code /review


## Images and Files

Users can send text, voice messages with transcription, images, mixed text plus images, files, and videos. Images are passed to the agent as image attachments. Files and videos are downloaded to temporary local paths so the agent can read them with file tools.

Assistant responses are sent as WeCom markdown. To send a local image generated by the agent, include one marker outside code blocks:

```text
[IMAGE: /absolute/path/to/image.png]
```

For safety, local image paths must be inside the channel file directory under the system temporary directory, such as `/tmp/channel-files/...`. Generic file, video, and voice upload markers are ignored because model-produced file paths could otherwise upload arbitrary workspace files.

## Troubleshooting

### Bot does not connect

- Verify the Bot ID and Secret.
- Make sure the robot is created in API mode.
- Check that the environment variables are available in the shell running `qwen channel start`.

### Bot does not respond in groups

- Check `groupPolicy`.
- Mention the bot unless the group config sets `"requireMention": false`.
- Confirm the robot has been added to the group.

### Self-built application credentials do not work

This channel is for WeCom intelligent robots. Self-built application callback credentials such as Corp ID, Agent ID, Token, and EncodingAESKey are not used by this channel.
30 changes: 28 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"packages/channels/telegram",
"packages/channels/weixin",
"packages/channels/dingtalk",
"packages/channels/wecom",
"packages/channels/feishu",
"packages/channels/qqbot",
"packages/channels/plugin-example",
Expand Down
18 changes: 13 additions & 5 deletions packages/channels/base/src/ChannelBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2146,25 +2146,33 @@ export abstract class ChannelBase {
return `${GROUP_HISTORY_CONTEXT_MARKER}\n${formatted.join('\n')}\n\n${CURRENT_MESSAGE_MARKER}\n${promptText}`;
}

async handleInbound(envelope: Envelope): Promise<void> {
// 1. Group gate: policy + allowlist + mention gating
protected async preflightInbound(envelope: Envelope): Promise<boolean> {
const groupResult = this.groupGate.check(envelope);
if (!groupResult.allowed) {
if (groupResult.reason === 'mention_required') {
this.recordPendingGroupHistory(envelope);
}
return; // silently drop — no pairing, no reply
return false;
}

// 2. Sender gate: allowlist / pairing / open
const result = this.gate.check(envelope.senderId, envelope.senderName);
if (!result.allowed) {
if (result.pairingCode !== undefined) {
await this.onPairingRequired(envelope.chatId, result.pairingCode);
}
return;
return false;
}

return true;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] instanceof Promise is fragile for the preflightInbound return type (boolean | Promise<boolean>). If a subclass returns a non-native thenable (e.g., from a library or custom async helper), instanceof Promise is false, the thenable is treated as a truthy boolean, and processInbound runs without waiting for the gate check — bypassing group gating, sender allowlisting, and pairing checks entirely.

// Suggested fix: always await — await on a non-Promise is a no-op
async handleInbound(envelope: Envelope): Promise<void> {
  if (!(await this.preflightInbound(envelope))) return;
  await this.processInbound(envelope);
}

This is already what WeComAdapter does in its direct call path. The micro-cost of await on a synchronous boolean is one microtask hop, negligible for inbound message handling.

async handleInbound(envelope: Envelope): Promise<void> {
if (!(await this.preflightInbound(envelope))) return;

await this.processInbound(envelope);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] processInbound is protected, meaning any future adapter subclass can call it directly without ever invoking preflightInbound, silently bypassing group policy, sender allowlist, and pairing gates.

The WeCom adapter correctly calls preflight then processInbound separately (to interleave attachment download), but there is no runtime enforcement of this invariant. A future channel author would naturally call this.processInbound(envelope) — it's a protected method on the base class — and skip all authorization.

Suggested fix: Add a private preflightPassed flag that preflightInbound sets and processInbound checks (throwing if not set). At minimum, add a JSDoc warning on processInbound that it must only be called after preflightInbound returns true.

protected async processInbound(envelope: Envelope): Promise<void> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] processInbound is protected and bypasses all security gates (group policy, sender allowlist, pairing). The collect-buffer drain path calls it directly (intentionally, to avoid re-preflighting coalesced messages), which normalizes the pattern. Nothing in the method's JSDoc warns future channel authors that calling it directly skips security.

A future adapter that calls processInbound for a 'special case' silently drops all gating.

Fix: Add a JSDoc warning:

/**
 * Process an inbound message AFTER preflight gates have passed.
 * WARNING: This method does NOT run group gating, sender allowlisting,
 * or pairing checks. Callers MUST run preflightInbound() first unless
 * the envelope was already preflighted (e.g., collect-buffer drain).
 */

// 3. Slash command handling — before session/agent routing

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical — future maintainer] processInbound is protected and bypasses all security gates (group gating, sender allowlisting, pairing). The JSDoc says "Callers must run preflightInbound() first" but there is no runtime enforcement.

The WeCom adapter calls preflightInbound + processInbound separately in onMessage, establishing a pattern that future adapter authors will copy. If they forget the preflightInbound call in one code path (e.g., a message-edit event handler, a retry path), unauthenticated messages reach the agent silently.

Suggested approaches (any of these):

  1. Return a PreflightToken from preflightInbound that processInbound requires as a parameter
  2. Set a private preflightPassed = new WeakSet<Envelope>() in preflightInbound, check it in processInbound
  3. At minimum, rename to unsafeProcessInbound to make the danger visible
  4. Add @internal JSDoc to signal these are not for general adapter use

const parsed = this.parseCommand(envelope.text);
if (parsed) {
Expand Down
29 changes: 29 additions & 0 deletions packages/channels/wecom/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@qwen-code/channel-wecom",
"version": "0.19.5",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This new workspace is versioned as 0.19.5, while the root package and the sibling channel packages in this PR are all 0.19.6; package-lock.json records the same mismatch for packages/channels/wecom. That leaves the release metadata inconsistent for publishing/version audits. Please bump this package and the corresponding lockfile entry to the current repo version.

Suggested change
"version": "0.19.5",
"version": "0.19.6",

— GPT-5 Codex via Qwen Code /review

"description": "WeCom channel adapter for Qwen Code",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc --build",
"test": "vitest run",
"test:ci": "vitest run"
},
"dependencies": {
"@qwen-code/channel-base": "file:../base",
"@wecom/aibot-node-sdk": "^1.0.7"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
Loading
Loading