Skip to content

feat(weixin): add WeChat channel plugin with QR login and UI#1704

Merged
kaizhou-lab merged 46 commits intomainfrom
feat/weixin-plugin
Mar 25, 2026
Merged

feat(weixin): add WeChat channel plugin with QR login and UI#1704
kaizhou-lab merged 46 commits intomainfrom
feat/weixin-plugin

Conversation

@piorpua
Copy link
Copy Markdown
Contributor

@piorpua piorpua commented Mar 25, 2026

Summary

  • Add WeChat (微信) channel plugin with iLink Bot long-poll implementation, replacing the removed weixin-agent-sdk dependency
  • Add QR code login flow, WeixinMonitor, WeixinAdapter, WeixinPlugin, and full IPC bridge wiring
  • Add WeChat channel UI: WeixinConfigForm, icon, and i18n translations across 6 locales (zh-CN: 微信, others: WeChat)

Test plan

  • WeChat channel appears in Channels list with green icon
  • Title shows "微信" in zh-CN, "WeChat" in all other languages
  • WeChat icon visible in Channels tab icon row (next to Telegram/Lark/DingTalk)
  • Description text includes 微信/WeChat across all locales
  • QR code login flow works: scan → connected status shown
  • Enabling/disabling the WeChat channel toggle works correctly
  • Agent and model selection persists for WeChat channel
  • All unit tests pass: bun run test

zynx added 30 commits March 23, 2026 10:40
- WeixinLogin: rewrite API calls to use GET+query params matching iLink SDK
  (bot_type param, correct response fields qrcode/qrcode_img_content,
  bot_token/ilink_bot_id, long-poll timeout treated as wait)
- WeixinLoginHandler: render QR page via hidden BrowserWindow to extract
  canvas data URL, since qrcode_img_content is a JS-rendered SPA
- WeixinPlugin: write SDK credential file (~/.openclaw/.../accounts/<id>.json)
  on initialize so weixin-agent-sdk start() can authenticate
- ChannelManager: add weixin to getPluginTypeFromId and builtinStartableTypes;
  add weixin credentials block in enablePlugin; always derive pluginType from
  pluginId instead of stale DB type field
- channelBridge: add weixin to BUILTIN_TYPES and BUILTIN_NAMES so getPluginStatus
  correctly returns hasToken and status for the WeChat channel
- database: include type in upsertChannelPlugin ON CONFLICT update so stale
  plugin type records are corrected on next enable
- Fix duplicate replies by writing WeChat credentials to AionUi's
  isolated data dir (getDataDir()) instead of ~/.openclaw, preventing
  the local openclaw gateway from auto-discovering the account
- Temporarily set OPENCLAW_STATE_DIR before void start() so the SDK
  reads the isolated dir, then restore after the first async suspension
- Add .then() auto-resolve on emitMessage for non-streaming responses
  (pairing codes, errors) that only call sendMessage without editMessage
- Strip HTML from sendMessage text stored in accumulatedText
- Add stripHtml to WeixinAdapter and wire it into ActionExecutor for
  outgoing text formatting on the weixin platform
- Add 'weixin' to ConversationSource and isFromChannel yoloMode check
- Fix TelegramConfigForm to filter pairings/events by platformType so
  Weixin pairing requests don't bleed into the Telegram UI
- Add pairing request and authorized user management sections to
  WeixinConfigForm, matching the pattern from TelegramConfigForm
- Update weixinPlugin unit tests to mock @/common/platform and use the
  isolated test data dir; fix pending-handler tests to block emitMessage
  so auto-resolve doesn't fire before rejection tests can observe it
zynx added 4 commits March 25, 2026 15:47
- Add weixin.svg channel logo (green WeChat-style icon)
- Register weixin logo in ChannelHeader channelLogoMap
- Add weixin logo to CHANNEL_LOGOS in WebuiModalContent Channels tab
- Add channels.weixinTitle / channels.weixinDesc i18n keys to all 6 locales
  (zh-CN: 微信, zh-TW: 微信, others: WeChat)
- Update webui.featureChannelsDesc to include WeChat across all locales
zynx added 3 commits March 25, 2026 17:05
- Move &amp; decoding to first position to prevent partial entity
  artifacts in output (fixes double-escaping/unescaping, CWE-116)
- Move tag stripping to last to catch tags formed from decoded
  entities like &lt;script&gt; (fixes incomplete sanitization, CWE-116)
- Add stripHtml unit tests covering XSS vectors and entity decoding
- Update WeixinLogin to use qrcode_url/ticket from QR endpoint
  and botToken/userId/baseUrl from poll endpoint (matching test contracts)
- Remove renderQRPage from WeixinLoginHandler: onQR now forwards
  the image URL directly to renderer instead of opening a hidden window
- Add missing channel mock entries (getPendingPairings, getAuthorizedUsers,
  pairingRequested, userAuthorized) to WeixinConfigForm DOM tests
- Fix oxfmt formatting issues in 5 files
- weixinLogin.test.ts: align mock responses with actual iLink API field
  names (qrcode/qrcode_img_content for QR endpoint, bot_token/ilink_bot_id/
  baseurl for poll endpoint)
- weixinLoginHandler.test.ts: use vi.spyOn on renderQRPage to avoid
  BrowserWindow mock complexity; mock electron at file level for import;
  test verifies onQR forwards canvas data URL to renderer IPC
zynx added 8 commits March 25, 2026 17:53
- Add formatError helper to capture error.cause for better diagnostics
- Split agent.chat() and callSendMessage() into separate try-catch blocks
- Log 'agent error' vs 'send error' to distinguish failure points
- No retry on send failure to avoid duplicate messages
…n stripHtml

- Strip tags before decoding entities to prevent double-unescaping
  (&amp;lt; now stays as &lt; instead of becoming <)
- Use single-pass replace callback for entity decoding (no chained replacements)
- Two-pass strip: before decode handles raw tags, after decode handles
  entity-encoded tags (&lt;script&gt; -> <script>)
- Loop strip until stable to handle nested/malformed markup

Fixes CodeQL alerts: js/double-escaping (#99), js/incomplete-multi-character-sanitization (#100)
@piorpua
Copy link
Copy Markdown
Contributor Author

piorpua commented Mar 25, 2026

Code Review:feat(weixin): add WeChat channel plugin with QR login and UI (#1704)

变更概述

本 PR 为 AionUI 添加了完整的微信渠道插件,包含:main process 端的 QR 码登录流程(WeixinLogin)、长轮询消息接收(WeixinMonitor)、typing 状态管理(WeixinTyping)、消息格式转换(WeixinAdapter)、插件主体(WeixinPlugin);以及 Renderer 端的配置表单(WeixinConfigForm)和 6 个语言的 i18n 翻译。同时修复了 TelegramConfigForm 在多渠道场景下接收跨平台配对请求的旧 bug。


方案评估

结论:✅ 方案合理

自行实现 WeChat iLink Bot HTTP 客户端替代被移除的 SDK 依赖的思路正确,Promise bridge 模式(handleChatsendMessage / editMessage)与其他 builtin plugin 保持一致。renderQRPage 通过隐藏 BrowserWindow 加载外部 URL 并提取 canvas 内容,在 WeChat API 只返回页面 URL 而非直接图片的场景下是合理的权宜之计。整体架构边界(main/renderer/IPC bridge)划分清晰,无混用。


问题清单

🟠 HIGH — console.log 直接透传到生产环境 main process

文件src/process/channels/plugins/weixin/WeixinPlugin.ts,第 84 行

问题代码

startMonitor({
  ...
  log: (msg) => console.log(msg),
});

问题说明WeixinMonitor 的长轮询循环会对每次 API 调用结果、每次重试、每次错误都调用 log()。直接传入 console.log 意味着这些信息会在生产环境持续输出到控制台,造成日志噪音。项目规范明确禁止 console.log,Stop hook 也会在每次 session 结束时对此发出警告。其他 builtin plugin(Telegram/Lark/DingTalk)均未使用 console.log 作为 logger。

修复建议:若项目暂无统一 logger,可直接移除(startMonitorlog 参数是可选的):

startMonitor({
  baseUrl: this.baseUrl,
  token: this.botToken,
  accountId: this.accountId,
  dataDir: getPlatformServices().paths.getDataDir(),
  agent: { chat: (req) => this.handleChat(req) },
  abortSignal: this.abortController.signal,
  // log omitted — defaults to no-op
});

🟡 MEDIUM — WeixinConfigForm.tsx 中 8 处 error: any 违反 strict mode

文件src/renderer/components/settings/SettingsModal/contents/channels/WeixinConfigForm.tsx,第 156、170、184、283 行及 (saved as any)

问题代码

} catch (error: any) {
  Message.error(error.message);
}

// 以及:
if (saved && typeof saved === 'object' && 'backend' in saved && typeof (saved as any).backend === 'string') {
  setSelectedAgent({
    backend: (saved as any).backend as AcpBackendAll,
    ...
  });
}

问题说明:oxlint 报告 8 条 no-explicit-any warning。项目 strict mode 明确禁止 anycatch (error: any) 是获取 .message 的常见误用,(saved as any) 则是绕过了 ConfigStorage.get 已有的类型定义。

修复建议

// catch 块:
} catch (error) {
  Message.error(error instanceof Error ? error.message : String(error));
}

// ConfigStorage 读取:
const saved = await ConfigStorage.get('assistant.weixin.agent');
// saved 类型已是 { backend: AcpBackendAll; customAgentId?: string; name?: string } | undefined
if (saved?.backend) {
  setSelectedAgent({
    backend: saved.backend,
    customAgentId: saved.customAgentId,
    name: saved.name,
  });
}

🔵 LOW — 16 处 no-await-in-loop lint 警告累积(需要 disable 注释)

文件WeixinMonitor.ts(10 处)、WeixinTyping.ts(2 处)、WeixinLogin.ts(3 处)

问题说明:所有的 await in loop 均为有意为之的顺序轮询/重试(长轮询、指数退避重试),不能也不应该并行化。lint rule 产生了 false positive。但这 16 条新增 warning 会降低代码库整体 lint 信噪比。

另外 WeixinMonitor.ts:214 有一条 no-unmodified-loop-condition warning:

while (!signal?.aborted) {  // ← lint 误报 signal 未被修改

修复建议:在对应循环头部添加 disable 注释:

// eslint-disable-next-line no-await-in-loop
const resp = await callGetUpdates(...);

// 对 no-unmodified-loop-condition:
// eslint-disable-next-line no-unmodified-loop-condition
while (!signal?.aborted) {

🔵 LOW — activeUsers 集合在运行期间从不清理

文件src/process/channels/plugins/weixin/WeixinPlugin.ts,第 115、119 行

问题代码

private handleChat(request: WeixinChatRequest): Promise<WeixinChatResponse> {
  const { conversationId } = request;
  this.activeUsers.add(conversationId);  // 只添加,从不删除
  ...
}

问题说明activeUsers 在每次 handleChat 时添加用户,但仅在 onStop()clear()。对长期运行的实例,集合会无限增长,导致 getActiveUserCount() 返回的是历史累积用户数而非「当前活跃」的数量。

修复建议:在 handleChat 的 Promise 完成/拒绝后从集合中移除:

return new Promise<WeixinChatResponse>((resolve, reject) => {
  ...
  this.emitMessage(unified)
    .then(() => {
      this.activeUsers.delete(conversationId);  // 完成后清理
      ...
    })
    .catch((error: unknown) => {
      this.activeUsers.delete(conversationId);  // 出错后也清理
      ...
    });
});

汇总

# 严重级别 文件 问题
1 🟠 HIGH WeixinPlugin.ts:84 console.log 直接透传到生产环境 main process
2 🟡 MEDIUM WeixinConfigForm.tsx:156,170,184,283 8 处 error: any 违反 strict mode
3 🔵 LOW WeixinMonitor.ts, WeixinTyping.ts, WeixinLogin.ts 16 处 no-await-in-loop lint warning 需要 disable 注释
4 🔵 LOW WeixinPlugin.ts:115 activeUsers 只增不减,长期运行内存增长

结论

⚠️ 有条件批准 — 存在 1 个 HIGH 问题(console.log 在生产代码中),需要处理后合并。MEDIUM 和 LOW 问题不阻塞合并但建议一并修复。整体实现质量高,测试覆盖充分,i18n 完整,架构边界清晰。


本报告由本地 pr-review skill 生成,包含完整项目上下文,无截断限制。

- Fix 8 no-explicit-any violations in WeixinConfigForm.tsx: replace
  catch (error: any) with proper unknown narrowing, and replace
  (saved as any) with typed Record<string, unknown> cast
- Add oxlint-disable-next-line comments for intentional no-await-in-loop
  patterns in WeixinMonitor.ts (10), WeixinTyping.ts (2), WeixinLogin.ts (3)
- Add oxlint-disable-next-line for no-unmodified-loop-condition on the
  abort-signal while loop in WeixinMonitor.ts

Review follow-up for #1704
@piorpua
Copy link
Copy Markdown
Contributor Author

piorpua commented Mar 25, 2026

PR Fix 验证报告

原始 PR: #1704
修复方式: 直接推送到 feat/weixin-plugin

# 严重级别 文件 问题 修复方式 状态
1 🟠 HIGH WeixinPlugin.ts:84 console.log 直接透传到生产环境 用户确认为故意保留(方便定位问题) ⏭️ 跳过
2 🟡 MEDIUM WeixinConfigForm.tsx:156,170,184,283 8 处 error: any 违反 strict mode catch (error: any) 改为 catch (error) 并用 error instanceof Error 安全取 message;(saved as any) 改为 as Record<string, unknown> + 具名类型 ✅ 已修复
3 🔵 LOW WeixinMonitor.ts, WeixinTyping.ts, WeixinLogin.ts 16 处 no-await-in-loop lint warning 在各循环中的 await 前添加 // oxlint-disable-next-line eslint/no-await-in-loop;Monitor:214 添加 no-unmodified-loop-condition disable 注释 ✅ 已修复
4 🔵 LOW WeixinPlugin.ts:115 activeUsers 只增不减 用户确认:getActiveUserCount() 仅用于 UI 展示(PluginManager.ts:272),不参与任何逻辑判断,语义偏差可接受 ⏭️ 跳过

总结: ✅ 已修复 2 个 | ⏭️ 跳过 2 个(用户确认)

验证: oxlint 0 warnings / 0 errors,tsc --noEmit 无类型错误,60 个测试全部通过。

@kaizhou-lab kaizhou-lab merged commit 57094e1 into main Mar 25, 2026
15 of 17 checks passed
@piorpua piorpua deleted the feat/weixin-plugin branch March 25, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants