Added a WeChat bridge leveraging Feishu and Tencent OpenClaw.#3206
Added a WeChat bridge leveraging Feishu and Tencent OpenClaw.#3206VincentCorleone wants to merge 1 commit into
Conversation
…-weixin/openclaw-weixin
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
|
Thanks @VincentCorleone for taking the time to contribute. This repository is observing a maintainer-managed PR intake gate in dry-run mode, so this pull request is staying open. This note helps maintainers prepare the allowlist before any enforcement is considered. Please read |
There was a problem hiding this comment.
Code Review
This pull request introduces the weixin-bridge integration, allowing personal WeChat accounts to control a local CodeWhale runtime via the Tencent iLink Bot protocol. The feedback focuses on improving robustness and responsiveness: specifically, avoiding blocking the long-polling loop by running command handlers asynchronously, wrapping JSON parsing in try-catch blocks to prevent crashes on non-JSON responses, ensuring timeouts are always cleared to prevent leaks, and adding defensive checks for potentially null or undefined objects like response.body and the long-polling response.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const command = parseCommand(text); | ||
| await handleCommand(fromUser, command).catch((error) => { | ||
| console.error( | ||
| `failed to handle command from=${fromUser} text=${text.slice(0, 100)}`, | ||
| error | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Awaiting handleCommand inside the message processing loop blocks the entire long-polling cycle. If a turn takes a long time to execute, the bridge will be completely unresponsive to other users, and control commands like /interrupt will not be received or processed until the active turn completes. Removing the await allows the command handler to run asynchronously, keeping the long-polling loop active and responsive.
// 命令路由
const command = parseCommand(text);
handleCommand(fromUser, command).catch((error) => {
console.error(
`failed to handle command from=${fromUser} text=${text.slice(0, 100)}`,
error
);
});| continue; | ||
| } | ||
|
|
||
| const data = JSON.parse(raw); |
There was a problem hiding this comment.
If apiGet returns a non-JSON string (such as "timeout" or gateway HTML error pages on 524), JSON.parse(raw) will throw a SyntaxError and crash the bridge. Wrapping the parsing in a try-catch block ensures that non-JSON responses are safely caught, allowing the polling loop to wait and retry.
let data;
try {
data = JSON.parse(raw);
} catch {
// If the response is not valid JSON (e.g. "timeout" or HTML error), wait and retry
await new Promise((r) => setTimeout(r, 1000));
continue;
}", edge_cases| const resp = await getUpdates({ | ||
| baseUrl, | ||
| token, | ||
| get_updates_buf: getUpdatesBuf, | ||
| timeoutMs: nextTimeoutMs, | ||
| signal: abortController.signal, | ||
| }); | ||
|
|
||
| clearTimeout(timer); |
There was a problem hiding this comment.
If getUpdates throws an error, clearTimeout(timer) is skipped, which leaks the timeout. Wrapping the call in a try...finally block ensures that the timer is always cleared, preventing potential memory leaks over time.
let resp;
try {
resp = await getUpdates({
baseUrl,
token,
get_updates_buf: getUpdatesBuf,
timeoutMs: nextTimeoutMs,
signal: abortController.signal,
});
} finally {
clearTimeout(timer);
}| async function* readSse(response) { | ||
| let buffer = ""; | ||
| for await (const chunk of response.body) { |
There was a problem hiding this comment.
If response.body is null (e.g., due to an empty response or network issue), attempting to iterate over it will throw a TypeError. Adding a defensive check for response.body prevents potential crashes.
| async function* readSse(response) { | |
| let buffer = ""; | |
| for await (const chunk of response.body) { | |
| async function* readSse(response) { | |
| if (!response.body) return; | |
| let buffer = ""; | |
| for await (const chunk of response.body) { |
| if (resp.longpolling_timeout_ms) { | ||
| nextTimeoutMs = resp.longpolling_timeout_ms; | ||
| } | ||
|
|
||
| // 检查错误 | ||
| const isApiError = | ||
| (resp.ret !== undefined && resp.ret !== 0) || | ||
| (resp.errcode !== undefined && resp.errcode !== 0); | ||
|
|
There was a problem hiding this comment.
If resp is null or undefined, accessing its properties will throw a TypeError and crash the loop. Using optional chaining and defensively checking resp prevents these crashes.
| if (resp.longpolling_timeout_ms) { | |
| nextTimeoutMs = resp.longpolling_timeout_ms; | |
| } | |
| // 检查错误 | |
| const isApiError = | |
| (resp.ret !== undefined && resp.ret !== 0) || | |
| (resp.errcode !== undefined && resp.errcode !== 0); | |
| if (resp?.longpolling_timeout_ms) { | |
| nextTimeoutMs = resp.longpolling_timeout_ms; | |
| } | |
| // 检查错误 | |
| const isApiError = | |
| !resp || | |
| (resp.ret !== undefined && resp.ret !== 0) || | |
| (resp.errcode !== undefined && resp.errcode !== 0); |
| if (resp.get_updates_buf) { | ||
| getUpdatesBuf = resp.get_updates_buf; | ||
| await saveSyncBuf(config.stateDir, getUpdatesBuf); | ||
| } | ||
|
|
||
| // 处理消息 | ||
| const msgs = resp.msgs || []; |
There was a problem hiding this comment.
If resp is null or undefined, accessing resp.get_updates_buf or resp.msgs will throw a TypeError. Using optional chaining ensures safe property access.
| if (resp.get_updates_buf) { | |
| getUpdatesBuf = resp.get_updates_buf; | |
| await saveSyncBuf(config.stateDir, getUpdatesBuf); | |
| } | |
| // 处理消息 | |
| const msgs = resp.msgs || []; | |
| if (resp?.get_updates_buf) { | |
| getUpdatesBuf = resp.get_updates_buf; | |
| await saveSyncBuf(config.stateDir, getUpdatesBuf); | |
| } | |
| // 处理消息 | |
| const msgs = resp?.msgs || []; |
Summary
To use CodeWhale from the WeChat infrastructure I already use every day, I added a WeChat bridge leveraging the existing Feishu Bridge in this repo and Tencent OpenClaw on npm.
Testing
I ran an end-to-end test (WeChat → CodeWhale) and confirmed it works: I send a message to CodeWhale on WeChat, and it replies with context-aware responses based on the working directory.
Checklist
22 of 24 CI checks have passed. One was skipped, which is not a concern. The remaining failure is caused by a missing CNB_GIT_TOKEN; since this token is configured in the upstream repository (Hunter Bown's), this check is expected to resolve automatically upon merge.