Skip to content

fix(weixin): refuse to send when contextToken is missing (avoid silent-drop) - #60

Merged
NewFuture merged 3 commits into
NewFuture:mainfrom
stwhwing:fix/weixin-silent-drop-contexttoken
Aug 10, 2026
Merged

NewFuture merged 3 commits into
NewFuture:mainfrom
stwhwing:fix/weixin-silent-drop-contexttoken

Conversation

@stwhwing

Copy link
Copy Markdown
Contributor

PR 描述正文(适配 NewFuture/openclaw-weixin 社区版)

本 PR 改编自上游 Tencent/openclaw-weixin#247(作者:stwhwing),把「缺少 contextToken 时静默丢弃消息却返回伪成功」的修复移植到社区版 main,并满足社区仓库 AGENTS.md / CONTRIBUTING.md 的硬性约束(错误信息脱敏、聚焦测试、补中英 changelog、不 bump 版本)。

问题来源

iLink 通道在 contextToken 缺失时,旧代码仅 logger.warn 后仍然调用 sendMessageApi。iLink 后端对该情况返回 HTTP 200 + 空 body,消息被后端丢弃,但调用方拿到的是本地生成的 messageId(伪成功)。这导致微信推送失败在日志侧“看起来成功”,实际用户收不到消息,且难以排查。

上游 #247 已认定该 bug 真实存在,社区版 main 当前仍复现(5 处 logger.warn → sendMessageApi)。

本 PR 的改动

1. src/messaging/send.ts —— 5 个发送入口缺 token 时直接抛错(而非 warn 后发送)

涉及:sendMessageWeixin / sendImageMessageWeixin / sendVideoMessageWeixin / sendFileMessageWeixin(及媒体项聚合路径)。

if (!opts.contextToken) {
  logger.error("sendMessageWeixin: contextToken missing — refusing to send (silent-drop risk; upstream #247)");
  throw new Error(
    "[openclaw-weixin] sendMessageWeixin: contextToken missing — refusing to send to avoid silent-drop (upstream issue #247)",
  );
}

2. 错误信息 / 日志脱敏(隐私合规,社区硬性要求)

  • 抛出的 Error.message 不含 contextToken,也不含 完整收件人 ID(to)。
  • 所有日志中的 to=${to} 改为 to=${redactToken(to)},复用社区既有 src/util/redact.tsredactToken(显示前 6 字符 + (len=N)),避免泄露用户标识。

3. 测试

  • 翻转 4 个既有「无 token 不抛错」测试refuses to send without contextToken (throws),断言 rejects.toThrow(/contextToken missing/)mockSendMessageApi 未被调用(保证真的没发出去,而非发了被丢弃)。
  • 新增 describe("missing contextToken refuses to send (silent-drop fix, upstream #247)")
    • 5 个函数各自「缺 token → 抛错且后端 API 未调用」;
    • 1 个隐私测试:抛出的错误不得包含完整收件人 ID(o9cq806PLhqoC5-fjuN63zCyAInQ@im.wechat);
    • 1 个反例:提供有效 contextTokensendMessageWeixin 正常发送成功。
  • 修复了既有测试里的 mock 一次性队列泄漏(beforeEach 改用 vi.resetAllMocks()),避免「re-throws API errors」误判为通过。

4. Changelog(不 bump 版本)

CHANGELOG.md(中文)/ CHANGELOG_EN.md(英文)的 ## [未发布] / ## [Unreleased] 段新增修复条目,注明静默丢弃修复与溯源 #247。

验证

  • npx vitest run src/messaging/send.test.ts23/23 通过
  • npx tsc --noEmit -p tsconfig.json:改动文件无类型错误
  • npx biome check0 错误(已自动格式化超长行)
  • 范围限定在「缺失 token」场景,不处理「token 过期」等其它分支(与上游 #247 范围一致),避免范围蔓延。

已知的无关失败

全量测试时 src/messaging/inbound-dedupe.test.ts1 个失败,但经在 pristine main 上复跑确认该失败为仓库既有问题(涉及文件系统状态、与本次改动无关)。本 PR 未改动该模块。

提交信息(建议)

fix(weixin): refuse to send when contextToken is missing (avoid silent-drop)

Adapted from Tencent/openclaw-weixin#247 (authored by stwhwing).
Throw on missing contextToken across all 5 send entry points instead of
warn-then-send, which caused messages to be silently dropped by the iLink
backend while the caller received a fake success messageId.

- Error messages and logs redacted via redactToken (no token / no full id).
- Flip 4 existing no-throw tests; add focused regression + counterexample tests.
- Add zh/en changelog entries under Unreleased (no version bump).

Source: https://github.com/Tencent/openclaw-weixin/pull/247

…t-drop)

Adapted from Tencent/openclaw-weixin#247 (authored by stwhwing).
Community edition (NewFuture/openclaw-weixin) still had 5 send entry
points that warned-then-sent without contextToken, causing iLink to
return HTTP 200 with empty body and silently drop the message while
the caller saw a fake success.

Changes:
- send.ts: throw on missing contextToken in all 5 send entry points
  (sendMessageWeixin, sendMessageItemWeixin, sendImageMessageWeixin,
  sendVideoMessageWeixin, sendFileMessageWeixin); error messages carry
  no raw token or full recipient id (redacted via redactToken).
- send.test.ts: flip 4 existing 'no throw' tests to assert throw +
  backend not called; add focused regression + valid-token counterexample.
- CHANGELOG: add Unreleased entry (zh + en); no version bump.

Verified: send.test.ts 23/23, tsc clean, biome 0 errors.
@stwhwing
stwhwing force-pushed the fix/weixin-silent-drop-contexttoken branch from 73db044 to 6864808 Compare August 10, 2026 10:49
@NewFuture
NewFuture requested a balanced review from Copilot August 10, 2026 11:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Prevents false-success sends when contextToken is missing and improves recipient-log redaction.

Changes:

  • Rejects five outbound send paths without a context token.
  • Adds regression/privacy tests and resets mocks between tests.
  • Updates bilingual changelogs.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/messaging/send.ts Adds token guards and recipient redaction.
src/messaging/send.test.ts Tests rejection, API suppression, and valid-token sending.
CHANGELOG.md Documents the fix in Chinese.
CHANGELOG_EN.md Documents the fix in English.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/messaging/send.ts
Comment thread src/messaging/send.test.ts
Comment thread src/messaging/send.test.ts
Comment thread CHANGELOG.md Outdated
Comment thread CHANGELOG_EN.md Outdated
@ericcaiwx-star

Copy link
Copy Markdown
Collaborator

协作备注(资深/最小改动视角,非合并)

总评:方向对,核心改动够小,值得合;建议先用很小补丁收口调用面合同,再 squash。

认同

  • 在 5 个 send* 入口 fail-closed,是正确 choke point;不在 channel/cron 各写一遍,符合最小改动。
  • 假成功(iLink 空 200 + 本地 messageId)→ 真失败,对多账号/主动发送排查很有价值。
  • 测试把旧合同「无 token 也能发」翻成「抛错且不打 API」,并有带 token 反例,行为钉得住。
  • 抛错文案本身不拼接收方 ID,这点好。

请用最小补丁收口(非重构)

  1. src/channel.ts:仍有 sending without context 后继续调用 send;合入后必抛,日志会误导。请改成与新合同一致的语义(明确将失败 / 或让错误干净冒泡),不要再写 “sending without…”。
  2. src/messaging/error-notice.ts:JSDoc 写无 token 则 no-op,实现却是 warn 后仍 send。合入后会进 catch。最小修法:无 token early return(真正 no-op),与注释对齐。
  3. Changelog:主卖点应是 refuse-to-send;「全路径脱敏」目前不属实(主要改了 send.ts 部分 log),请收窄表述,避免过度承诺。
  4. 测试(可选):下方整组 5 入口用例与上面 per-function refuse 用例有重复,可合并减噪;FULL_ID 请换成明显合成标识(符合 AGENTS.md),不必用生产形态 peer id。

结论

支持修这个 bug。上述 1–3 是小收口,不是要求扩大 scope。收口后我这边没有反对合入的理由。

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Suppressed comments (5)

src/messaging/send.test.ts:346

  • This privacy test checks only Error.message; it never exercises or inspects the changed logger calls at send.ts:95,138,188,193,220. A regression that logs the complete recipient on API failure or media success would still pass. Add logger assertions for the text failure and media success/failure paths, verifying that combined log messages exclude the full synthetic ID and contain only its redacted form.
  it("does not leak the full recipient id in the thrown error (privacy)", async () => {

src/messaging/send.test.ts:285

  • This opaque value has the shape of a live Weixin account identifier and is not clearly synthetic. CONTRIBUTING.md:111-112 requires removing account identifiers from tests; use an unmistakably synthetic recipient value so a real identifier cannot be published as a privacy fixture.
  const FULL_ID = "o9cq806PLhqoC5-fjuN63zCyAInQ@im.wechat";

src/messaging/send.ts:214

  • This guard runs only after the production media path has downloaded/encrypted and uploaded the file: sendWeixinMediaFile performs each CDN upload before invoking these image/video/file functions (src/messaging/send-media.ts:28-67, and src/channel.ts:273-291 may download first). A missing-token media request therefore still creates an unnecessary sensitive CDN artifact before being rejected. Validate contextToken at the media boundary before any download/upload, and test that the upload APIs remain untouched.
  if (!opts.contextToken) {
    logger.error("sendImageMessageWeixin: contextToken missing — refusing to send (silent-drop risk; upstream #247)");

CHANGELOG_EN.md:18

  • This claim is broader than the implementation: recipient IDs are still logged verbatim by the same outbound paths, for example src/channel.ts:121, src/messaging/send-media.ts:29-58, and src/messaging/process-message.ts:369,424,435,449,461. Either redact those call sites too or scope the changelog statement to the diagnostics changed in these five functions.
  generated fake-success `messageId`. These helpers redact recipient IDs in their
  own logs via `redactToken`
  (see Tencent/openclaw-weixin#247).

CHANGELOG.md:16

  • 这里的表述超出了实际改动范围:相同出站链路仍会在 src/channel.ts:121src/messaging/send-media.ts:29-58src/messaging/process-message.ts:369,424,435,449,461 明文记录接收方 ID。请同时脱敏这些调用点,或把 changelog 限定为本次修改的五个函数内部日志。
  token 时返回本地生成的「假成功」`messageId`。这些入口自身的接收方日志改用
  `redactToken` 脱敏(见 Tencent/openclaw-weixin#247)。

Comment thread src/messaging/send.test.ts
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/messaging/send.test.ts:285

  • This value resembles a real Weixin recipient identifier, and recipient IDs are sensitive. Tests must use unmistakably synthetic identifiers so private account data cannot be committed; the redaction assertion remains valid with a synthetic @im.wechat value.
  const FULL_ID = "o9cq806PLhqoC5-fjuN63zCyAInQ@im.wechat";

src/messaging/send.test.ts:356

  • This privacy test checks only the thrown error, while the PR also changes several diagnostic log sites. The repository contract requires tests to prove sensitive values are absent from diagnostics. Add assertions against the mocked logger for representative text failure and media success/failure paths, verifying the synthetic recipient never appears verbatim.
  it("does not leak the full recipient id in the thrown error (privacy)", async () => {
    let thrown: unknown;
    try {
      await sendMessageWeixin({ to: FULL_ID, text: "hi", opts: { baseUrl: "https://api.com" } });
    } catch (e) {
      thrown = e;
    }
    expect(thrown).toBeInstanceOf(Error);
    const msg = thrown instanceof Error ? thrown.message : String(thrown);
    // the recipient peer id must never appear verbatim in the error surface
    expect(msg).not.toContain(FULL_ID);

src/messaging/error-notice.ts:19

  • This warning still writes the complete recipient ID, contradicting the redaction goal and the repository privacy contract. The recipient is unnecessary to explain this skip, so omit it (or redact it).
    logger.warn(`sendWeixinErrorNotice: no contextToken for to=${params.to}, skipping error notice`);

src/messaging/send.ts:217

  • This guard is reached only after the media pipeline has already downloaded and uploaded the file: sendWeixinMediaFile performs its CDN upload before calling this helper (src/messaging/send-media.ts:28-53). A missing token therefore still consumes network/CDN resources and uploads content that cannot be delivered. Validate immediately after the sending hook and before remote download/CDN upload in both outbound paths, and test that upload mocks remain untouched.
  if (!opts.contextToken) {
    logger.error("sendImageMessageWeixin: contextToken missing — refusing to send (silent-drop risk; upstream #247)");
    throw new Error(
      "[openclaw-weixin] sendImageMessageWeixin: contextToken missing — refusing to send to avoid silent-drop (upstream issue #247)",
    );

@NewFuture
NewFuture merged commit ec51ef4 into NewFuture:main Aug 10, 2026
11 checks passed
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.

4 participants