Skip to content

feat(weixin): support channel file send protocol#1993

Merged
piorpua merged 7 commits intoiOfficeAI:mainfrom
JerryLiu369:feat/weixin-file-send
Mar 31, 2026
Merged

feat(weixin): support channel file send protocol#1993
piorpua merged 7 commits intoiOfficeAI:mainfrom
JerryLiu369:feat/weixin-file-send

Conversation

@JerryLiu369
Copy link
Copy Markdown
Member

Summary

  • add a channel-scoped weixin-file-send skill and protocol parsing for Weixin conversations
  • support Weixin media upload/send for local image and file outputs from channel conversations
  • align upload and send ordering with the OpenClaw Weixin plugin and add channel-focused tests

Test plan

  • bunx tsc --noEmit
  • bun run test tests/unit/channels/weixinMonitor.test.ts tests/unit/channels/weixinPlugin.test.ts tests/unit/channels/weixinSystemActions.test.ts

@sentry
Copy link
Copy Markdown

sentry bot commented Mar 31, 2026

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b8d044803

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +94 to +95
if (workspace && !isPathInsideWorkspace(resolvedPath, workspace) && resolvedPath !== path.resolve(workspace)) {
continue;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Canonicalize media path before workspace boundary check

The workspace restriction currently relies on lexical path comparison, so a symlink inside the workspace can bypass it. For example, if workspace/leak points to /etc/passwd, isPathInsideWorkspace still passes and statSync follows the symlink, allowing files outside the workspace to be sent. This is a data-exfiltration risk for channel conversations; resolve the candidate with realpath (or reject symlinks with lstat) before enforcing the workspace boundary.

Useful? React with 👍 / 👎.

Comment on lines +382 to +384
const fileData = fs.readFileSync(action.path);
if (fileData.length > UPLOADS_MAX_BYTES) {
throw new Error(`file too large: ${fileData.length}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce upload size limit before reading file into memory

uploadMediaAction reads the whole file with fs.readFileSync and only then checks UPLOADS_MAX_BYTES. If a media action points to a very large file, the process can consume excessive memory (or OOM) before the guard runs. This can be triggered by model output paths in normal channel flows, so the size should be validated via stat (or a bounded stream) before reading file contents.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd6b2a476f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

await callSendMediaMessage(baseUrl, token, wechatUin, conversationId, uploaded, msg.context_token);
} catch (sendErr) {
const failedName = mediaAction.fileName || path.basename(mediaAction.path);
fallbackNotices.push(`Failed to send ${failedName}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Localize media-send failure notice

When a media upload/send fails, this hardcoded English fallback is appended to the user-visible reply (Failed to send ...). /workspace/AionUi/AGENTS.md explicitly requires that all user-facing text use i18n keys, so this introduces a non-localized runtime message that will leak English text to non-English chats whenever file delivery fails.

Useful? React with 👍 / 👎.

Comment on lines +87 to +91
const resolvedPath = path.isAbsolute(action.path)
? path.resolve(action.path)
: workspace
? path.resolve(workspace, action.path)
: '';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject protocol paths when conversation workspace is missing

If getConversation(conversationId) fails (for example, stale/deleted conversation records or transient DB read failures), workspace becomes undefined, but absolute protocol paths are still accepted and only checked for existence/size. In that state, a model-emitted block like /etc/passwd bypasses workspace confinement entirely and can be uploaded to chat; this code should fail closed unless a valid workspace root is available.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eee9eee910

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +101 to +103
if (!resolvedPath) continue;
if (!existsSync(resolvedPath)) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit failure notice when protocol actions are discarded

This branch silently drops unresolved protocol actions (continue) before they reach the upload loop, but no fallback text is generated for those drops. In the common “file-only” skill output (no visible text, only [AIONUI_CHANNEL_SEND]), an invalid/missing/out-of-workspace path results in mediaActions becoming empty and response.text staying empty, so WeixinMonitor sends nothing at all (its fallback notices are only added inside the upload/send catch path). This makes user requests appear to be ignored instead of reporting a failed send.

Useful? React with 👍 / 👎.

@piorpua piorpua added the bot:reviewing Review in progress (mutex) label Mar 31, 2026
@piorpua
Copy link
Copy Markdown
Contributor

piorpua commented Mar 31, 2026

Code Review:feat(weixin): support channel file send protocol (#1993)

变更概述

本 PR 为微信渠道新增文件/图片发送能力。核心改动包括:(1) 新增 channelSendProtocol.ts 解析 AI 回复中的 [AIONUI_CHANNEL_SEND] 协议块,提取媒体发送指令;(2) 在 WeixinMonitor.ts 中实现完整的媒体上传(AES-128-ECB 加密 + CDN 上传)和发送流程;(3) 在 channelConversation.ts 中为微信会话自动注入 weixin-file-send skill;(4) 新增对应的 skill 文件供 AI agent 使用。改动涉及 src/process/channels/ 下多个模块及 6 个语言的 i18n 文件。


方案评估

结论:✅ 方案合理

方案整体设计清晰:通过 skill 文件引导 AI 在回复中嵌入结构化协议块,由 channelSendProtocol.ts 解析并验证(含 workspace 沙箱检查和符号链接逃逸防护),再由 WeixinMonitor 执行上传和发送。与 OpenClaw Weixin 插件的发送顺序保持一致(先 caption 再 media 最后 text)。buildChannelConversationExtra 统一了各 backend 的 extra 构建逻辑,消除了之前仅 default 分支传递 backend/customAgentId 的不一致。


问题清单

🟡 MEDIUM — channelSendProtocol.ts 使用 statSync 重复调用

文件src/process/channels/utils/channelSendProtocol.ts,第 105-109 行

问题代码

const pathInfo = lstatSync(resolvedPath);
const canonicalPath = realpathSync(resolvedPath);
if (!isPathInsideWorkspace(canonicalPath, workspaceRoot)) continue;

const stats = pathInfo.isSymbolicLink() ? statSync(canonicalPath) : statSync(resolvedPath);

问题说明:当路径不是符号链接时,lstatSync(resolvedPath)statSync(resolvedPath) 返回相同结果,但调用了两次。可以复用 pathInfo

修复建议

const pathInfo = lstatSync(resolvedPath);
const canonicalPath = realpathSync(resolvedPath);
if (!isPathInsideWorkspace(canonicalPath, workspaceRoot)) continue;

const stats = pathInfo.isSymbolicLink() ? statSync(canonicalPath) : pathInfo;

🟡 MEDIUM — WeixinMonitor.ts 同步读取整个文件到内存

文件src/process/channels/plugins/weixin/WeixinMonitor.ts,第 390 行

问题代码

const fileData = fs.readFileSync(action.path);

问题说明:上限 200MB,同步读取会阻塞事件循环。虽然当前在 monitor 循环中 sequential 运行影响有限,但作为面向生产的上传功能,建议使用 fs.promises.readFile 并在函数签名中保持 async。

修复建议

const fileData = await fs.promises.readFile(action.path);

🔵 LOW — CHANNEL_SEND_BLOCK_RE 正则使用 /g 标志可能存在状态问题

文件src/process/channels/utils/channelSendProtocol.ts,第 12 行

问题代码

const CHANNEL_SEND_BLOCK_RE = /\[AIONUI_CHANNEL_SEND\]\s*([\s\S]*?)\s*\[\/AIONUI_CHANNEL_SEND\]/g;

问题说明:模块级 /g 正则在 String.prototype.replace 中使用是安全的(replace 不会修改 lastIndex)。但如果将来有人用 exec()test() 调用此正则,lastIndex 状态会导致交替匹配失败。当前无问题,仅作提示。


🔵 LOW — Patch 覆盖率 55%,WeixinMonitor.ts 新增上传逻辑 patch 覆盖仅 48%

文件src/process/channels/plugins/weixin/WeixinMonitor.ts

问题说明:Codecov 报告 patch 覆盖率 55.26%,其中 WeixinMonitor.ts 为 48.14%(27 行缺失 + 15 行 partial)。核心上传函数 callGetUploadUrluploadBufferToCdncallSendMediaMessage 的分支覆盖不完整。虽然 codecov/patch 设为 informational 不阻塞合并,但建议后续补充针对 CDN 上传重试逻辑和错误分支的单元测试。


汇总

# 严重级别 文件 问题
1 🟡 MEDIUM channelSendProtocol.ts:105-109 statSync 重复调用,可复用 lstatSync 结果
2 🟡 MEDIUM WeixinMonitor.ts:390 同步读取大文件阻塞事件循环
3 🔵 LOW channelSendProtocol.ts:12 模块级 /g 正则潜在状态问题(当前安全)
4 🔵 LOW WeixinMonitor.ts Patch 覆盖率偏低,上传逻辑测试不足

结论

⚠️ 有条件批准 — 存在两个 MEDIUM 级别的小问题(statSync 重复调用和同步文件读取),处理后可合并。


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

CONCLUSION: CONDITIONAL
IS_CRITICAL_PATH: false
CRITICAL_PATH_FILES: (none)
PR_NUMBER: 1993

@piorpua piorpua added bot:ready-to-fix CONDITIONAL review done, waiting for bot fix bot:fixing Fix in progress (mutex) and removed bot:reviewing Review in progress (mutex) bot:ready-to-fix CONDITIONAL review done, waiting for bot fix labels Mar 31, 2026
- Reuse lstatSync result instead of redundant statSync call in channelSendProtocol
- Replace sync readFileSync with async fs.promises.readFile in WeixinMonitor

Review follow-up for iOfficeAI#1993
@piorpua
Copy link
Copy Markdown
Contributor

piorpua commented Mar 31, 2026

PR Fix 验证报告

原始 PR: #1993
修复方式: 直接推送到 feat/weixin-file-send

# 严重级别 文件 问题 修复方式 状态
1 🟡 MEDIUM channelSendProtocol.ts:109 statSync 重复调用 复用 lstatSync 返回的 pathInfo 替代冗余 statSync 调用 ✅ 已修复
2 🟡 MEDIUM WeixinMonitor.ts:390 同步读取大文件阻塞事件循环 替换 readFileSync 为 await fs.promises.readFile ✅ 已修复

总结: ✅ 已修复 2 个 | ❌ 未能修复 0 个

🔵 LOW 级别问题已跳过(不阻塞合并,修复优先级低)。

@piorpua piorpua enabled auto-merge (squash) March 31, 2026 13:51
@piorpua piorpua added bot:done Auto-merged by bot and removed bot:fixing Fix in progress (mutex) labels Mar 31, 2026
@piorpua piorpua merged commit 6b47d79 into iOfficeAI:main Mar 31, 2026
16 of 18 checks passed
mingxuanzi pushed a commit to mingxuanzi/AionUi that referenced this pull request Apr 1, 2026
* feat(weixin): support channel file send protocol

* fix(weixin): align media upload with openclaw

* fix(weixin): align media send ordering

* fix(weixin): broaden file send skill triggers

* fix(weixin): simplify file send skill description

* fix(weixin): harden channel file send

* fix(weixin): address review issues from PR iOfficeAI#1993

- Reuse lstatSync result instead of redundant statSync call in channelSendProtocol
- Replace sync readFileSync with async fs.promises.readFile in WeixinMonitor

Review follow-up for iOfficeAI#1993

---------

Co-authored-by: zynx <>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:done Auto-merged by bot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants