> ## Documentation Index
> Fetch the complete documentation index at: https://autclaw.shop/llms.txt
> Use this file to discover all available pages before exploring further.

# autClaw plugin media sending

> Send images, voice, video, files, and mixed messages from autClaw JavaScript and Python plugins.

Use media sending APIs to reply to the current conversation with images, voice, video, files, or mixed content. You can also push media proactively to a specific IM target.

<Note>
  New plugins should prefer `Sender` methods: `replyImage`, `replyVoice`, `replyVideo`, `replyFile`, and `replyMixed`. Legacy global functions such as `sendImage`, `sendVoice`, `sendVideo`, `sendFile`, and `sendMixed` still work for maintaining old reply scripts, but new examples use `Sender`.
</Note>

## Choose a sending method

<CardGroup cols={2}>
  <Card title="Reply to the current chat" icon="reply">
    Use `sender.replyImage(...)` and related methods in a message-triggered plugin. The target comes from the current message.
  </Card>

  <Card title="Push proactively" icon="send">
    Use `pushImage(...)`, `pushVoice(...)`, `pushVideo(...)`, `pushFile(...)`, or `pushMixed(...)` with `imType`, group, and user IDs.
  </Card>

  <Card title="Send combined content" icon="list">
    Use `replyMixed(...)` to send text, images, voice, video, or files in one message sequence.
  </Card>

  <Card title="Save files first" icon="download">
    Use `fileDownload(...)` to save a remote URL, base64 payload, or data URI, then pass the local path to a media method.
  </Card>
</CardGroup>

## Quick example

<CodeGroup>
  ```js JavaScript theme={null}
  //[author: your-name]
  //[runtime: node@22.22.1]
  //[rule: image]
  const { Sender, getSenderID } = require('./middleware.js')

  const sender = new Sender(getSenderID())
  const receipt = await sender.replyImage('https://example.com/demo.png')

  if (receipt.ok === false) {
    await sender.reply(`Image send failed: ${receipt.error || 'unknown error'}`)
  }
  ```

  ```python Python theme={null}
  #[author: your-name]
  #[runtime: python@3.12]
  #[rule: image]
  from middleware import Sender, getSenderID

  sender = Sender(getSenderID())
  receipt = sender.replyImage("https://example.com/demo.png")

  if receipt.get("ok") is False:
      sender.reply(f"Image send failed: {receipt.get('error') or 'unknown error'}")
  ```
</CodeGroup>

## Media sources

The `source` parameter of media methods supports these forms:

| Source              | Example                         | Best for                                                   |
| ------------------- | ------------------------------- | ---------------------------------------------------------- |
| Remote URL          | `https://example.com/a.png`     | Files already hosted publicly or reachable by the adapter. |
| Local absolute path | `/opt/autclaw/plugin/tmp/a.png` | Files generated by the plugin.                             |
| Local relative path | `./tmp/a.png`                   | Files near the plugin runtime directory.                   |
| base64              | `base64://...`                  | Sending generated binary content directly.                 |
| data URI            | `data:image/png;base64,...`     | Base64 content with a MIME type.                           |

<Tip>
  Local paths are best for files generated by plugins, such as screenshots, reports, TTS audio, or downloaded temporary files. Remote URLs work well when the asset is already publicly reachable.
</Tip>

## Reply with media

Create `Sender` first, then call the reply method. The return value is usually `SendReceipt`.

<CodeGroup>
  ```js JavaScript theme={null}
  const { Sender, getSenderID } = require('./middleware.js')

  const sender = new Sender(getSenderID())

  await sender.replyImage('https://example.com/a.png')
  await sender.replyVoice('./voice.wav')
  await sender.replyVideo('./demo.mp4')
  await sender.replyFile('./report.csv', { filename: 'report.csv' })
  ```

  ```python Python theme={null}
  from middleware import Sender, getSenderID

  sender = Sender(getSenderID())

  sender.replyImage("https://example.com/a.png")
  sender.replyVoice("./voice.wav")
  sender.replyVideo("./demo.mp4")
  sender.replyFile("./report.csv", {"file_name": "report.csv"})
  ```
</CodeGroup>

### Method purpose

| Method                         | Purpose               | Common sources                              |
| ------------------------------ | --------------------- | ------------------------------------------- |
| `replyImage(source, options?)` | Send an image.        | `png`, `jpg`, `gif`, image URL, base64.     |
| `replyVoice(source, options?)` | Send voice/audio.     | `wav`, `mp3`, `ogg`, `opus`, audio URL.     |
| `replyVideo(source, options?)` | Send video.           | `mp4`, `mov`, video URL.                    |
| `replyFile(source, options?)`  | Send a file.          | `csv`, `txt`, `pdf`, `zip`, or other files. |
| `replyMixed(items, options?)`  | Send a mixed message. | Text + image/voice/video/file.              |

### options

The second parameter of media reply methods is an optional object. Current regular plugin helpers use these fields:

| Field                                 | Type     | Description                                    |
| ------------------------------------- | -------- | ---------------------------------------------- |
| `filename` / `fileName` / `file_name` | `string` | Displayed or saved filename.                   |
| `mimeType` / `mime_type`              | `string` | MIME type, such as `image/png` or `video/mp4`. |

To specify a target, use `pushImage(...)`, `pushVoice(...)`, `pushVideo(...)`, `pushFile(...)`, or `pushMixed(...)`. Regular replies send to the current conversation by default.

## Send mixed messages

`replyMixed(items, options?)` combines text and media into one message sequence. Media items can use `source`, `file`, `url`, or `path` for the source.

<CodeGroup>
  ```js JavaScript theme={null}
  await sender.replyMixed([
    { type: 'text', text: 'Done: ' },
    { type: 'image', source: './result.png', name: 'result.png' },
    { type: 'file', source: './report.csv', name: 'report.csv', mime_type: 'text/csv' },
  ])
  ```

  ```python Python theme={null}
  sender.replyMixed([
      {"type": "text", "text": "Done: "},
      {"type": "image", "source": "./result.png", "name": "result.png"},
      {"type": "file", "source": "./report.csv", "name": "report.csv", "mime_type": "text/csv"},
  ])
  ```
</CodeGroup>

Supported `items`:

| item                                | Required field | Description                                            |
| ----------------------------------- | -------------- | ------------------------------------------------------ |
| `{ type: 'text', text: '...' }`     | `text`         | Send a plain text segment.                             |
| `{ type: 'markdown', text: '...' }` | `text`         | Send a Markdown segment, depending on adapter support. |
| `{ type: 'image', source: '...' }`  | `source`       | Send an image.                                         |
| `{ type: 'voice', source: '...' }`  | `source`       | Send voice/audio.                                      |
| `{ type: 'video', source: '...' }`  | `source`       | Send video.                                            |
| `{ type: 'file', source: '...' }`   | `source`       | Send a file.                                           |

Media items can also include `name`, `file_name`, `filename`, `mime_type`, or `mimeType`.

## Push media proactively

Proactive push does not depend on the current message. Specify the target:

* `imType`: IM type, such as `qq` or `wx`. Use the adapters enabled in your deployment.
* `groupCode`: Group number or group ID. Usually an empty string for private chats.
* `userID`: User ID. Usually an empty string for group pushes.
* `title`: Push title or note. Some adapters ignore it.

<CodeGroup>
  ```js JavaScript theme={null}
  const { pushImage, pushVideo, pushMixed } = require('./middleware.js')

  await pushImage('qq', '123456', '', 'Daily report', './daily.png', { account_id: 'bot_qq_ops' })
  await pushVideo('qq', '123456', '', 'Demo video', './demo.mp4', { account_id: 'bot_qq_ops' })
  await pushMixed('qq', '123456', '', 'Result', [
    { type: 'text', text: 'Result:' },
    { type: 'image', source: './result.png' },
  ], { account_id: 'bot_qq_ops' })
  ```

  ```python Python theme={null}
  from middleware import pushImage, pushVideo, pushMixed

  pushImage("qq", "123456", "", "Daily report", "./daily.png", {"account_id": "bot_qq_ops"})
  pushVideo("qq", "123456", "", "Demo video", "./demo.mp4", {"account_id": "bot_qq_ops"})
  pushMixed("qq", "123456", "", "Result", [
      {"type": "text", "text": "Result:"},
      {"type": "image", "source": "./result.png"},
  ], {"account_id": "bot_qq_ops"})
  ```
</CodeGroup>

| Method      | Parameters                                                         | Returns                        |
| ----------- | ------------------------------------------------------------------ | ------------------------------ |
| `pushImage` | `(imType, groupCode, userID, title, imageUrl, options?, timeout?)` | Send count, usually `0` or `1` |
| `pushVoice` | `(imType, groupCode, userID, title, voiceUrl, options?, timeout?)` | Send count                     |
| `pushVideo` | `(imType, groupCode, userID, title, videoUrl, options?, timeout?)` | Send count                     |
| `pushFile`  | `(imType, groupCode, userID, title, fileUrl, options?, timeout?)`  | Send count                     |
| `pushMixed` | `(imType, groupCode, userID, title, items, options?, timeout?)`    | Send count                     |

For proactive push account selection rules, see [`JavaScript proactive push`](/en/plugin-sdk/javascript/push) or [`Python proactive push`](/en/plugin-sdk/python/push).

## Save and send files

`fileDownload(url, path?)` saves `http(s)`, `base64://...`, or `data:*;base64,...` sources as local files. Pass the returned `path` / `file_path` to media sending methods.

<CodeGroup>
  ```js JavaScript theme={null}
  const { Sender, getSenderID, fileDownload } = require('./middleware.js')

  const sender = new Sender(getSenderID())
  const file = await fileDownload('data:image/png;base64,...', 'images')

  await sender.replyImage(file.path, { filename: file.file_name, mimeType: file.mime_type })
  ```

  ```python Python theme={null}
  from middleware import Sender, getSenderID, fileDownload

  sender = Sender(getSenderID())
  file = fileDownload("data:image/png;base64,...", "images")

  sender.replyImage(file["path"], {"file_name": file["file_name"], "mime_type": file.get("mime_type")})
  ```
</CodeGroup>

### fileDownload parameters

| Parameter | Type     | Required | Description                                                                                                                     |
| --------- | -------- | :------: | ------------------------------------------------------------------------------------------------------------------------------- |
| `url`     | `string` |    Yes   | Supports `http(s)`, `base64://...`, and `data:*;base64,...`.                                                                    |
| `path`    | `string` |    No    | Save directory. If omitted, saves under runtime `file`; relative paths save under `file/<path>`; absolute paths are used as-is. |

### FileDownloadResult

```json theme={null}
{
  "path": "/opt/autclaw/file/images/demo.png",
  "file_path": "/opt/autclaw/file/images/demo.png",
  "file_name": "demo.png",
  "mime_type": "image/png",
  "file_size": 12345
}
```

A single `fileDownload(...)` file is limited to about `20 MB` by default. Download failures, unreachable sources, or oversized files throw exceptions.

## Handle user-uploaded files

If a user uploads a file in a message, read media items first, then call `downloadAdapterFile(...)` to save it locally.

<CodeGroup>
  ```js JavaScript theme={null}
  const { Sender, getSenderID, downloadAdapterFile } = require('./middleware.js')

  const sender = new Sender(getSenderID())
  const items = await sender.getMediaItems()
  const fileItem = items.find((item) => item.type === 'file')

  if (!fileItem) {
    await sender.reply('Please send a file')
  } else {
    const saved = await downloadAdapterFile(fileItem, { path: 'imports' })
    await sender.reply(`Saved: ${saved.file_name}`)
  }
  ```

  ```python Python theme={null}
  from middleware import Sender, getSenderID, downloadAdapterFile

  sender = Sender(getSenderID())
  items = sender.getMediaItems()
  file_item = next((item for item in items if item.get("type") == "file"), None)

  if not file_item:
      sender.reply("Please send a file")
  else:
      saved = downloadAdapterFile(file_item, {"path": "imports"})
      sender.reply(f"Saved: {saved.get('file_name')}")
  ```
</CodeGroup>

`downloadAdapterFile(...)` returns the same fields as `fileDownload(...)` and may include extra fields:

```json theme={null}
{
  "ok": true,
  "url": "https://adapter.example/file?id=...",
  "action": "resolveFileURL"
}
```

## SendReceipt

Text, media, and recall methods usually return a send receipt. Adapters may add extra fields.

```json theme={null}
{
  "ok": true,
  "message_id": "123456",
  "messageid": "123456",
  "message_ids": ["123456"],
  "raw": {},
  "action": "sendImage"
}
```

When checking results, treat `ok === false` / `ok is False` as failure. Do not rely on only one message ID field.

<CodeGroup>
  ```js JavaScript theme={null}
  const receipt = await sender.replyVideo('./demo.mp4')

  if (receipt.ok === false) {
    await sender.reply(`Video send failed: ${receipt.error || 'unknown error'}`)
  } else {
    console.log('message id:', receipt.message_id || receipt.messageid)
  }
  ```

  ```python Python theme={null}
  receipt = sender.replyVideo("./demo.mp4")

  if receipt.get("ok") is False:
      sender.reply(f"Video send failed: {receipt.get('error') or 'unknown error'}")
  else:
      print("message id:", receipt.get("message_id") or receipt.get("messageid"))
  ```
</CodeGroup>

## Practical tips

* Before sending a locally generated file, make sure it has been fully written.
* Set `filename` / `name` for files and mixed media items so users can recognize downloads.
* Prefer reachable URLs for large files. Save to disk with `fileDownload(...)` only when you need local processing.
* Do not manually parse user-uploaded platform raw fields. Use `sender.getMediaItems()` + `downloadAdapterFile(...)`.
* When media sending fails, read `error` from the receipt and give users a concrete next step.

## Legacy reply script entry points

When maintaining legacy `plugin/replies/*.js` scripts, you can still use global functions:

```js theme={null}
sendImage('https://example.com/a.png')
sendVoice('./voice.wav')
sendVideo('./demo.mp4')
sendFile('./report.csv')
sendMixed([
  { type: 'text', text: 'Result: ' },
  { type: 'image', source: './result.png' },
])
```

For new `plugin/scripts/*.js` or `plugin/scripts/*.py` files, prefer the `Sender` style for readability and type hints.
