Skip to content

fix(remote): deliver weixin generated images#1610

Merged
zerob13 merged 1 commit into
devfrom
fix/weixin-generated-image-delivery
May 11, 2026
Merged

fix(remote): deliver weixin generated images#1610
zerob13 merged 1 commit into
devfrom
fix/weixin-generated-image-delivery

Conversation

@yyhhyyyyyy
Copy link
Copy Markdown
Collaborator

@yyhhyyyyyy yyhhyyyyyy commented May 11, 2026

deliver weixin generated images
image

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced image handling with support for multiple source formats and improved persistence
    • Secure encrypted image transmission for enhanced data protection
  • Improvements

    • Better image caching and session-aware storage management

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

📝 Walkthrough

Walkthrough

This PR adds end-to-end support for assistant-generated images: decoding from multiple source formats (cached paths, data URLs, raw base64), persisting them with workspace or userData roots, and sending them encrypted via iLink protocol to WeChat.

Changes

Generated Images: Multi-Format Resolution, Storage, and Encrypted Delivery

Layer / File(s) Summary
Foundation: Imports and Constants
src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts, src/main/presenter/remoteControlPresenter/weixinIlink/weixinIlinkClient.ts
Add Electron app import and generated-asset constants including expanded MIME mappings (BMP, AVIF). Add crypto imports and iLink channel version constant for encrypted media flow.
Image Content Resolution Helpers
src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts
Implement MIME normalization, file-extension inference, safe imgcache:// path resolution and decoding, base64 validation, and a unified async resolver returning { data, mimeType } for multiple source formats.
Workspace Resolution Refactoring
src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts
Introduce resolveOptionalAssetWorkspace returning null when unavailable; update resolveAssetWorkspace to throw only when required. Add resolveGeneratedImageAssetRoot using optional workspace or falling back to Electron userData.
Image Persistence in RemoteConversationRunner
src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts
Update generated image persistence to resolve asset root via optional workspace logic and write content via resolveGeneratedImageContent with correct MIME-type extension selection.
RemoteConversationRunner Test Infrastructure
test/main/presenter/remoteControlPresenter/remoteConversationRunner.test.ts
Mock Electron app.getPath to return consistent defaults for path resolution.
Generated Image Persistence Tests
test/main/presenter/remoteControlPresenter/remoteConversationRunner.test.ts
Validate persistence from imgcache://, data URLs, and raw base64 with correct MIME/extensions; verify persistence to userData when workspace is unavailable.
iLink Protocol: Types and Helpers
src/main/presenter/remoteControlPresenter/weixinIlink/weixinIlinkClient.ts
Define iLink API response and base-info types; introduce AES-128-ECB, MD5, and key-encoding utilities.
iLink Encrypted Image Sending
src/main/presenter/remoteControlPresenter/weixinIlink/weixinIlinkClient.ts
Refactor sendImageMessage to encrypt image bytes, request upload URL, POST encrypted content to CDN, and send message with encrypted metadata. Update uploadCdnMedia to extract x-encrypted-param header. Add base_info.channel_version to POST payloads.
WeixinIlinkRuntime Test Infrastructure
test/main/presenter/remoteControlPresenter/weixinIlinkRuntime.test.ts
Refactor createRuntime to accept constructor overrides; add createInboundMessage helper for flexible test fixtures.
Image Delivery via iLink Tests
test/main/presenter/remoteControlPresenter/weixinIlinkRuntime.test.ts
Validate deliverConversation sends generated images via sendImageMessage with encryption metadata and suppresses text messages.

Sequence Diagram

sequenceDiagram
  participant AssistantOutput
  participant RemoteConversationRunner
  participant ContentResolver as resolveGeneratedImageContent
  participant AssetRoot as resolveGeneratedImageAssetRoot
  participant DiskStorage as File System
  participant WeixinClient
  participant UploadService as CDN Upload
  participant WeChat

  AssistantOutput->>RemoteConversationRunner: generatedImages (imgcache://, data URL, base64)
  RemoteConversationRunner->>AssetRoot: resolveGeneratedImageAssetRoot(workspace)
  AssetRoot-->>RemoteConversationRunner: asset root (workspace or userData)
  RemoteConversationRunner->>ContentResolver: resolveGeneratedImageContent(source)
  ContentResolver-->>RemoteConversationRunner: { data, mimeType }
  RemoteConversationRunner->>DiskStorage: write file with extension from mimeType
  RemoteConversationRunner-->>RemoteConversationRunner: persist snapshot with imagePath, mimeType
  RemoteConversationRunner->>WeixinClient: sendImageMessage(imagePath, mimeType)
  WeixinClient->>WeixinClient: encrypt image bytes with AES key
  WeixinClient->>UploadService: POST encrypted bytes, extract x-encrypted-param
  UploadService-->>WeixinClient: success, x-encrypted-param header
  WeixinClient->>WeChat: send message with encrypt_query_param, aes_key, encrypt_type
  WeChat-->>WeixinClient: delivery success
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • ThinkInAIXYZ/deepchat#1380: Both PRs enhance workspace/workdir resolution infrastructure; this PR adds optional workspace resolution for generated-image storage, complementing the workspace-aware session management in that PR.

Poem

🐰 A rabbit's verse on images so fine,
From base64, cache paths, URLs they shine—
Encrypted with AES, to WeChat they fly,
Through iLink's secure tunnel, up to the sky!
One workspace, one userData, all assets align. 🎨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: enabling delivery of Weixin-generated images. The changeset modifications to RemoteConversationRunner, WeixinIlinkClient, and their tests all directly support this core functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/weixin-generated-image-delivery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
src/main/presenter/remoteControlPresenter/weixinIlink/weixinIlinkClient.ts (3)

612-641: ⚡ Quick win

Consider a longer timeout for CDN media uploads.

uploadCdnMedia reuses WEIXIN_ILINK_REQUEST_TIMEOUT_MS (15s), which is shared with lightweight JSON RPCs. AI-generated images frequently land in the 1–5 MB range, and on slower mobile/uplink networks a 15s budget for the full POST + CDN ack can be tight. Failures here force the whole sendImageMessage flow to start over (including a fresh getuploadurl).

Consider introducing a dedicated upload timeout (e.g. 60s) or making it configurable.

♻️ Proposed change — dedicated media upload timeout
 const WEIXIN_ILINK_REQUEST_TIMEOUT_MS = 15_000
 const WEIXIN_ILINK_LONG_POLL_TIMEOUT_MS = 35_000
+const WEIXIN_ILINK_MEDIA_UPLOAD_TIMEOUT_MS = 60_000
 const WEIXIN_ILINK_CHANNEL_VERSION = '1.0.0'
-    return await withTimeout(WEIXIN_ILINK_REQUEST_TIMEOUT_MS, async (signal) => {
+    return await withTimeout(WEIXIN_ILINK_MEDIA_UPLOAD_TIMEOUT_MS, async (signal) => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/remoteControlPresenter/weixinIlink/weixinIlinkClient.ts`
around lines 612 - 641, uploadCdnMedia uses the generic
WEIXIN_ILINK_REQUEST_TIMEOUT_MS (15s) which is too short for multi-MB CDN POSTs;
introduce a dedicated, longer timeout (e.g. WEIXIN_ILINK_CDN_UPLOAD_TIMEOUT_MS
defaulting to ~60000ms) or make it configurable, and replace the call to
withTimeout(...) inside uploadCdnMedia to use this new timeout constant/setting
so large media uploads get a higher deadline without affecting lightweight JSON
RPCs.

625-625: 💤 Low value

Avoid unnecessary buffer copy in CDN upload body.

Uint8Array.from(params.content) allocates and copies the encrypted bytes again. Buffer already extends Uint8Array, so params.content can be passed directly to fetch. For multi-MB images this doubles transient memory for no benefit.

♻️ Proposed fix
-          body: Uint8Array.from(params.content),
+          body: params.content,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/remoteControlPresenter/weixinIlink/weixinIlinkClient.ts`
at line 625, The fetch request is creating an unnecessary copy by using
Uint8Array.from(params.content); instead pass params.content directly as the
body to avoid duplicating multi-MB buffers—locate the fetch/HTTP upload call in
weixinIlinkClient (the code that sets body: Uint8Array.from(params.content)) and
replace that expression with params.content (or ensure it's typed as Buffer |
Uint8Array) so no extra allocation occurs.

172-179: ⚡ Quick win

Preserve original error context when wrapping image-send stage errors.

withImageSendStage re-throws as a plain Error, which discards the original error type (e.g. WeixinIlinkApiError) along with its status and errcode properties. Callers can no longer distinguish a CDN HTTP failure from an API errcode, which makes upstream retry/handling and logs less actionable.

♻️ Proposed fix — attach `cause` and preserve `WeixinIlinkApiError`
 const withImageSendStage = async <T>(stage: string, operation: () => Promise<T>): Promise<T> => {
   try {
     return await operation()
   } catch (error) {
     const message = error instanceof Error ? error.message : String(error)
-    throw new Error(`Weixin iLink image ${stage} failed: ${message}`)
+    const wrappedMessage = `Weixin iLink image ${stage} failed: ${message}`
+    if (error instanceof WeixinIlinkApiError) {
+      throw new WeixinIlinkApiError(wrappedMessage, error.status, error.errcode)
+    }
+    throw new Error(wrappedMessage, { cause: error })
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/remoteControlPresenter/weixinIlink/weixinIlinkClient.ts`
around lines 172 - 179, The helper withImageSendStage currently catches errors
and throws a new Error, losing the original error type and metadata (e.g.
WeixinIlinkApiError with status and errcode); change it to rethrow preserving
the original error as the cause (or rethrow the original instance when it is
already a WeixinIlinkApiError) and when creating a new Error include the
original error as the cause property so callers can inspect status/errcode;
update withImageSendStage to check instanceof WeixinIlinkApiError and either
throw the original error or throw a new Error(`Weixin iLink image ${stage}
failed: ${message}`, { cause: error }) to preserve context.
src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts (1)

727-737: Consider lifecycle/cleanup for the userData fallback asset root.

When no workspace is available, generated images are persisted indefinitely under userData/remote-assets/<channel>/<hash>/<messageId>/. There is no cleanup path tied to session deletion or message expiry, so over time this directory grows unbounded — especially for users heavily using image-generating agents on bots without a projectDir. Worth tracking a retention/pruning job (e.g. age-based or session-bound) before this becomes a support issue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts`
around lines 727 - 737, When resolveGeneratedImageAssetRoot falls back to
userData (the path built with REMOTE_GENERATED_ASSET_ROOT), ensure those
generated-asset directories are lifecycle-managed: add a registry and pruning
job for app.getPath('userData')/REMOTE_GENERATED_ASSET_ROOT that records
creation timestamps (or ties dirs to session IDs) and periodically removes
entries older than a retention TTL or when a session is deleted. Concretely,
update resolveGeneratedImageAssetRoot and/or resolveOptionalAssetWorkspace call
sites to (a) create session-scoped subdirectories under
REMOTE_GENERATED_ASSET_ROOT that include the session.agentId or message id, (b)
register the created path with a new cleanup service (e.g.,
GeneratedAssetCleaner) and (c) wire that cleaner to run at startup and on
session end to delete expired dirs; use the constants REMOTE_ASSET_ROOT and
REMOTE_GENERATED_ASSET_ROOT to locate targets and ensure the cleaner uses safe
delete (verify ownership) and a configurable TTL.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts`:
- Around line 727-737: When resolveGeneratedImageAssetRoot falls back to
userData (the path built with REMOTE_GENERATED_ASSET_ROOT), ensure those
generated-asset directories are lifecycle-managed: add a registry and pruning
job for app.getPath('userData')/REMOTE_GENERATED_ASSET_ROOT that records
creation timestamps (or ties dirs to session IDs) and periodically removes
entries older than a retention TTL or when a session is deleted. Concretely,
update resolveGeneratedImageAssetRoot and/or resolveOptionalAssetWorkspace call
sites to (a) create session-scoped subdirectories under
REMOTE_GENERATED_ASSET_ROOT that include the session.agentId or message id, (b)
register the created path with a new cleanup service (e.g.,
GeneratedAssetCleaner) and (c) wire that cleaner to run at startup and on
session end to delete expired dirs; use the constants REMOTE_ASSET_ROOT and
REMOTE_GENERATED_ASSET_ROOT to locate targets and ensure the cleaner uses safe
delete (verify ownership) and a configurable TTL.

In `@src/main/presenter/remoteControlPresenter/weixinIlink/weixinIlinkClient.ts`:
- Around line 612-641: uploadCdnMedia uses the generic
WEIXIN_ILINK_REQUEST_TIMEOUT_MS (15s) which is too short for multi-MB CDN POSTs;
introduce a dedicated, longer timeout (e.g. WEIXIN_ILINK_CDN_UPLOAD_TIMEOUT_MS
defaulting to ~60000ms) or make it configurable, and replace the call to
withTimeout(...) inside uploadCdnMedia to use this new timeout constant/setting
so large media uploads get a higher deadline without affecting lightweight JSON
RPCs.
- Line 625: The fetch request is creating an unnecessary copy by using
Uint8Array.from(params.content); instead pass params.content directly as the
body to avoid duplicating multi-MB buffers—locate the fetch/HTTP upload call in
weixinIlinkClient (the code that sets body: Uint8Array.from(params.content)) and
replace that expression with params.content (or ensure it's typed as Buffer |
Uint8Array) so no extra allocation occurs.
- Around line 172-179: The helper withImageSendStage currently catches errors
and throws a new Error, losing the original error type and metadata (e.g.
WeixinIlinkApiError with status and errcode); change it to rethrow preserving
the original error as the cause (or rethrow the original instance when it is
already a WeixinIlinkApiError) and when creating a new Error include the
original error as the cause property so callers can inspect status/errcode;
update withImageSendStage to check instanceof WeixinIlinkApiError and either
throw the original error or throw a new Error(`Weixin iLink image ${stage}
failed: ${message}`, { cause: error }) to preserve context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 27a41e5d-ce68-41ea-a854-54e7565d429f

📥 Commits

Reviewing files that changed from the base of the PR and between cf841aa and 2a736b1.

📒 Files selected for processing (4)
  • src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts
  • src/main/presenter/remoteControlPresenter/weixinIlink/weixinIlinkClient.ts
  • test/main/presenter/remoteControlPresenter/remoteConversationRunner.test.ts
  • test/main/presenter/remoteControlPresenter/weixinIlinkRuntime.test.ts

@zerob13 zerob13 merged commit 28d3162 into dev May 11, 2026
3 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.

2 participants