---
url: /wave-agent/specs/ui/image-pasting.md
description: >-
  从剪贴板粘贴图片到聊天输入，支持占位符和附件；粘贴时就降到 2000 像素预算，工具产图 / Read 图片在入列表时降到预算内，出站再按 2000 像素
  / 5MB 兜底
---

# 功能规格说明：图片粘贴

**创建日期**：2026-03-24

## 用户场景与测试 *（必填）*

### 用户故事：从剪贴板粘贴图片（优先级：P1）

作为用户，我希望能够使用 `Ctrl+V` 将剪贴板中的图片粘贴到聊天输入框中，以便轻松与 AI 分享截图或图片。

**为什么是这个优先级**：这是功能的核心。它允许用户向 AI 提供视觉上下文。

**独立测试**：可以通过复制图片到剪贴板，在聊天输入框中按 `Ctrl+V`，并验证输入文本中出现类似 `[Image #1]` 的占位符来测试。

**验收场景**：

1. **假设** 剪贴板中有图片，**当** 用户在聊天输入框中按 `Ctrl+V`（Windows 终端下为 `Alt+V`，见「跨平台支持」），**则** 创建临时图片文件，并在光标位置插入占位符 `[Image #1]`。
2. **假设** 粘贴了多张图片，**当** 用户多次按 `Ctrl+V`（Windows 下为 `Alt+V`），**则** 每张图片获得唯一 ID 和占位符（例如 `[Image #1]`、`[Image #2]`）。

***

### 用户故事：发送附带图片的消息（优先级：P1）

作为用户，我希望按 Enter 时粘贴的图片随消息一起发送，以便 AI 能够看到并处理它们。

**为什么是这个优先级**：确保视觉上下文实际传递给 AI。

**独立测试**：可以通过粘贴图片、输入消息、按 Enter，并验证发送给 AI 的消息包含图片数据来测试。

**验收场景**：

1. **假设** 消息中有图片占位符 `[Image #1]`，**当** 用户提交消息，**则** 图片文件路径和 MIME 类型包含在发送给 AI 服务的消息载荷中。
2. **假设** 消息中有多个图片占位符，**当** 用户提交消息，**则** 所有引用的图片都包含在消息载荷中。

***

### 用户故事：已发送图片的标签点开就能看（优先级：P1）

作为用户，我希望消息列表里已发送的图片标签点开就能看到这张图，不管这个会话跑在本机还是 SSH 远端主机上。

**为什么是这个优先级**：粘贴图在发送那一刻就被落成**会话所在那台机器**上的临时文件；这一版起，消息块里**同时**保留内联字节（内存与转录都留，对齐 Claude Code）与那个路径（给模型的 `[Image source: <path>]` 元数据用）。于是标签点开有两条路：块里有字节就地弹层，块里只剩路径（本版之前的会话，或下发远控页时剥过字节）才交宿主去读。判据是「字节在不在这一份消息里」；「宿主能不能打开本地文件」只在没有字节时兜底——此前把后者当成唯一判据，导致 SSH 会话里拿本机路径去加载远端图片（2026-10-30 用户报的裂图）与三端预览面各不相同。

**独立测试**：在 SSH 远端会话里粘贴一张图并发送，点开消息里的图片标签，验证就地弹层显示且全程没有向宿主发打开请求；用一份只有路径的老消息点开，验证桌面端走右侧文件面板、插件端走各自的图片预览；远控 Web 页上点开同一类消息，验证标签仍在原位、顺序不变但不可点。

**验收场景**：

1. **假设** 消息块里带内联字节，**当** 用户点开标签，**则** 就地弹层预览——桌面端、VS Code、JetBrains 表现一致，不请求宿主、不在 IDE 里新开编辑器标签。
2. **假设** 消息块里只有路径，**当** 用户点开标签，**则** 交给宿主打开：桌面端在右侧文件面板里内联显示（宿主按该分屏所在主机去读），插件端用它自己的图片预览命令打开（VS Code 打开内置图片预览，JetBrains 用内置图片编辑器——随 IDE 捆绑的 Images 插件认领 png/jpg/gif 等格式；只有内置编辑器认领不了该文件时才交给系统默认看图程序）；渲染层不得把该路径直接塞进 `<img src>` 自行加载。
3. **假设** 会话跑在 SSH 远端主机上，**当** 用户点开本版之后发送的图片，**则** 照场景 1 就地弹层（字节就在消息里，与主机无关）；只剩路径的老消息按场景 2 由宿主经该主机读回，文件已不在时按文件面板既有的「路径已不在」呈现。
4. **假设** 宿主是远控 Web 页（下发的消息里图片字节已被剥离，见 `remote/remote-web.md` 场景 21），**当** 用户看到带图片的消息，**则** 标签照旧显示、顺序与正文里的 `[imageN]` 一一对应，但不渲染成可点入口（与 `remote/remote-web.md` 场景 20 的路径同一条判据：不留「看着能点、点了没反应」）。

***

### 用户故事：草稿里的图片（优先级：P1，对齐 claude.ai / Claude Code 桌面端）

作为用户，我希望已经粘进输入框、还没发送的图片在我切换视图或页面重载后仍然在，以便我不必重新截图粘贴。

**为什么是这个优先级**：粘贴的图片目前只活在输入框的那几个标签节点上。设置页、插件市场、创作集、会话状态看板都会整页顶替对话区，把输入框连同标签一起卸载；回来时只能靠会话侧保存的草稿字符串重建输入框，而图片数据不在那份字符串里，用户看到的就只剩一段占位文本。桌面端的做法是把未发送的图片按会话存在本地存储里，草稿文本本身仍由会话侧保存——文本与图片分开存，重挂载时再拼回去。

**独立测试**：粘贴一张图、打几个字，打开设置页再返回，验证图片标签仍在原位且能点开预览；切到别的会话再切回，验证图片跟着会话走；发送后验证模型侧收到的是图片，而不是一个文件路径。

**验收场景**：

1. **假设** 输入框里有一张未发送的粘贴图片，**当** 用户打开设置页或会话状态看板（都整页顶替对话区、导致输入框卸载）再返回对话窗口，**则** 图片标签仍在原来的位置、仍可点开预览，与文字的先后顺序不变。
2. **假设** 某个会话的输入框里有一张未发送的粘贴图片，**当** 用户切到另一个会话，**则** 新会话的输入框不显示这张图；**当** 用户切回原会话，**则** 图片恢复——草稿图片按会话隔离，新对话按 pane 隔离。
3. **假设** webview 重载（窗口重开、面板重建）后宿主回放同一份草稿，**当** 输入框挂载，**则** 图片从本地存储回填；本地存储里已找不到这份图片数据时，只保留占位文本，不报错、不阻塞发送。
4. **假设** 草稿里有图片，**当** 用户发送消息，**则** 图片仍作为图片块随用户消息发出、模型直接可见，不得替换成「文件引用 + 让模型读文件」。
5. **假设** 本地存储不可用（隐私模式、配额已满），**当** 用户粘贴图片，**则** 图片照常进入输入框并能正常发送，只是这次不进入持久化草稿——静默降级，不弹错、不拦输入。

***

### 用户故事：跨平台支持（优先级：P2）

作为用户，我希望图片粘贴在 macOS、Windows 和 Linux 上都能工作，以便无论操作系统如何都有一致的体验。

**为什么是这个优先级**：确保 CLI 工具的所有用户都能使用此功能。

**验收场景**：

1. **假设** 用户在 macOS 上，**当** 他们粘贴图片，**则** 使用 `osascript` 读取剪贴板。
2. **假设** 用户在 Windows 上，**当** 他们按 `Alt+V` 粘贴图片，**则** 使用 PowerShell 读取剪贴板。*Windows 终端将 `Ctrl+V` 保留为系统文本粘贴，按键不会到达 CLI，因此 CLI 侧使用 `Alt+V`（与 Claude Code 一致）；终端若透传 `Ctrl+V` 亦可用。*
3. **假设** 用户在 Linux 上，**当** 他们粘贴图片，**则** 使用 `xclip` 读取剪贴板。

***

### 用户故事：图片源路径元数据（优先级：P1，对齐 Claude Code）

作为用户，我希望代理在收到图片时能看到图片的本地文件路径（包括从 webview 粘贴的图片），这样我可以写一个指定视觉模型的 subagent 来读取并描述图片内容。

**为什么是这个优先级**：Claude Code 在图片后附加 `[Image source: <path>]` 元数据文本，使模型能引用本地文件进一步处理。当前 Wave 仅发送 base64，模型不知道图片来自哪个文件，无法用工具（如 Read/Bash）操作图片文件。webview 粘贴的图片以 dataURL 形式到达 CLI，同样没有真实文件路径，需要 CLI 先将其落盘为临时文件。**落盘只补路径、不替换字节**：消息块里同时留内联字节（内存与转录都留，见「已发送图片的标签点开就能看」）和这个路径，路径只用于给模型的元数据与工具引用。

**独立测试**：可以构造带本地文件路径的图片消息，验证转换后的 API 消息在图片块后包含 `[Image source: <path>]` 文本；构造 dataURL 图片消息，验证落盘后消息块里仍带内联字节、且在图片块后附加 `[Image source: <落盘路径>]` 文本，落盘文件可读；无法落盘时不附加该文本、字节照发。

**验收场景**：

1. **假设** 图片有对应的本地文件路径（如 CLI 粘贴创建的临时文件），**当** 消息转换为 API 格式时，**则** 在图片块后附加文本 `[Image source: <路径>]`
2. **假设** 图片以 dataURL 到达（如 webview 通过 stdio 传入），**当** CLI 收到图片，**则** 先落盘为临时文件、把路径记在消息块上供上述元数据使用，**同时保留块里的内联字节**
3. **假设** 图片既没有本地文件路径也无法落盘，**当** 消息转换为 API 格式时，**则** 不附加元数据文本，仅发送图片
4. **假设** 会话从转录恢复，**当** 用户点开这些图片，**则** 字节从转录里的消息块读回即可就地预览，`[Image source: <路径>]` 仍用块里保存的路径，不重新落盘

***

### 用户故事：发送前校验图片有效性（优先级：P1）

作为用户，我希望粘贴一张无效图片时能立刻在本地被拦下并告诉我原因，而不是等消息发出去后收到一条来自上游的 `HTTP 400 ... unsupported image` 报错，以便我知道该怎么办。

**为什么是这个优先级**：剪贴板里的图片字节可以坏掉，而链路上没有任何一层会检查它。已复现的两条路径：① 剪贴板存在注册格式 `PNG` 但字节是坏的时候，Chromium 逐字节透传，坏字节原样进入消息；② 复制一个 0 字节（或截断、改名）的图片文件时，我们的链路会发出一张**空图** `data:image/png;base64,`。两种情况下上游都会返回 400，用户看到的是一条英文报错，且不知道该重试还是换图。

**独立测试**：构造 0 字节图片 / 文本改名成 `.png` / 头合法但内容损坏的 PNG，粘贴进输入框，验证不出现图片占位符、不产生图片消息并出现中文提示；粘贴合法 PNG/JPEG 验证照常插入占位符。

**验收场景**：

1. **假设**剪贴板中的图片数据为空（0 字节），**当**用户粘贴，**则**不插入图片占位符、不发送该图，并给出可见提示说明数据不完整、请重新截图或另存为 PNG/JPEG 后重试。
2. **假设**图片字节的头签名不属于 PNG / JPEG / GIF / WebP（例如 SVG、BMP、AVIF、HEIC，或一段被改名成 `.png` 的文本），**当**用户粘贴，**则**拒绝该图并提示仅支持这四种格式。
3. **假设**图片头签名在白名单内但内容已损坏或被截断（例如 PNG 缺少尾部 `IEND`、JPEG 缺少尾部 `FF D9`，或浏览器原生解码失败），**当**用户粘贴，**则**拒绝该图并给出与场景 1 相同的"数据不完整/无效"提示。
4. **假设**图片是合法的 PNG / JPEG / GIF / WebP，**当**用户粘贴，**则**行为与现状一致：插入 `[imageN]` 占位符并随消息一起发送。
5. **假设**输入框里已经贴进了一张不合格的图（例如从历史会话恢复），**当**消息转换为 API 请求时，**则**不再把空字节或未知格式伪装成 `image/png` 发出去——读不到/读空的图片直接跳过，未知扩展名不再冒充 PNG。
6. **假设**运行环境没有浏览器原生图片解码能力（如 jsdom、老宿主），**当**用户粘贴，**则**退化为头尾字节校验（不因缺少解码 API 而拒掉所有图片）。

***

### 用户故事：粘贴时在本地把图片降到 2000 像素预算（优先级：P1，对齐 Claude Code / claude.ai）

作为用户，我希望粘贴一张很长的截图（例如整页网页或长聊天记录）时它在本地就被缩到预算内，而不是收到一条看不懂的英文 400 报错，也不是让一张尺寸合规但超出预算的图白烧 token。

**为什么是这个优先级**：粘贴路径手上有宿主解码器与 canvas（`createImageBitmap` + `canvas`，零依赖），所以把我们主动选的 **2000 像素预算**（口径与理由见「出站图片按 2000 像素 / 5MB 预算收口」）直接做在这一步，而不是只守网关那道 8192 硬边界：

1. **一步到位**——否则同一张图会先在浏览器缩到 8192、再在 Node 侧缩到 2000：两次重编码白做工，且中间那一跳若体积偏大还会落到 JPEG，叠成二次有损。
2. 天然满足网关的 8192 硬边界（边界证据与实测见「出站图片按 2000 像素 / 5MB 预算收口」）。
3. **这一层不依赖按需下载的 sharp** ⇒ 即使本机图像能力装不上、SDK 侧只能降级，粘贴路径也落在预算内（降级路径见「无法在本机缩小时…」）。

竞品做的是同一件事：Claude Code 客户端按 2000 像素（sharp，Node 侧）；claude.ai 前端粘贴时就用 canvas 缩到 2000×2000（另加 512KB 体积预算）。

**独立测试**：构造任一边 2001 的图，验证粘贴后进入消息的是"任一边 ≤ 2000 且仍是白名单格式"的重编码副本；构造 2000 或以内的图，验证粘贴这一步没有重编码（仍是原始字节）。

**验收场景**：

1. **假设**粘贴的图片任一边超过 2000 像素，**当**用户粘贴，**则**在本地等比缩放到任一边不超过 2000 后插入占位符，并以数据 URL 形式随消息发送。
2. **假设**粘贴的图片任一边不超过 2000 像素，**当**用户粘贴，**则**粘贴这一步不做重编码，保持原始字节（含体积超过 5MB 的图——体积由「出站图片按 2000 像素 / 5MB 预算收口」处理，粘贴这步不因体积重编码）。
3. **假设**本地缩放在该宿主上不可行（没有 canvas、取不到 2d 上下文或转码失败），**当**用户粘贴，**则**按「发送前校验图片有效性」场景 3 的"数据不完整或无效"提示拒发该图，不新增尺寸类文案。
4. **假设**源图是 JPEG，**当**降采样，**则**输出 JPEG；源图是 PNG / GIF / WebP（可能带透明通道），**则**输出 PNG。

***

### 用户故事：工具产图与 Read 图片在入口就降到预算内，列表里只留降过的那份（优先级：P1，对齐 Claude Code）

作为长期在一条会话里干活的用户，我希望模型用 Read 读到的长截图、MCP / Exec 工具产出的图片在**进入消息列表的那一刻**就已经降到预算内，而不是等每一轮发请求时才临时缩小，以便这些图片的字节只以「降过的那一份」存在——agent 内存、宿主侧消息缓存、主进程到渲染进程的传输、渲染进程里都是同一份小图，会话攒下几十张截图也不会把内存拖垮。

**为什么是这个优先级**：目前预算只在**出站**那道闸上生效（见下一条故事）：`convertMessagesForAPI` 缩小的结果只进当次请求体，消息列表里的图片字节始终是原件。于是一张 4K 截图在进程里至少存在四份拷贝——agent 的消息列表、宿主侧消息缓存（桌面端每个 pane 一个 `StdioAgent` 各一份）、跨进程传输、渲染进程——而桌面端渲染进程崩溃的现场就是这份常驻（实测 `workingSet=2981 MB`、`messages=1842`）。Claude Code 的做法是把预算放在**入口**：图片进列表前就重编码到位，之后 API 与界面共用同一份小图。这里照此收口，出站那道闸保留为兜底（历史消息、本机缩不动的图、直接给路径的图）。

**独立测试**：构造一张 4000×3000 的工具结果图片，验证写入消息块的是缩小后的字节（解出的尺寸 ≤2000、长度远小于原件）且原件被落盘、消息里带路径；构造一张 1200×800 的图，验证消息块里的字节与工具返回的逐字相同、没有任何落盘。

**验收场景**：

1. **假设**工具结果带一张预算内的图（任一边 ≤2000 且 base64 ≤5MB），**当**它被写进消息块，**则**字节与 `mediaType` 与工具返回的逐字相同，不做重编码、不落盘。
2. **假设**工具结果的图任一边超过 2000 像素或 base64 超过 5MB，**当**它被写进消息块，**则**先按「出站图片按 2000 像素 / 5MB 预算收口」同一套阶梯缩放/压缩，**回写进消息块的是降过的那份**，原件不以 base64 形式继续留在内存里。
3. **假设**一张图被降过，**当**原件有可留的落点（Read 读到的本地文件、以路径贴进来的图本来就在盘上；MCP / Exec 凭空产出的图则落盘为临时文件），**则**消息块里带上指向原件的路径，模型与用户可以按路径裁掉不需要的部分后重新读取；落盘失败时保留原始字节，不因写盘失败而丢图。
4. **假设**消息里的图在入口已经降过，**当**它出站，**则**原样发送（不再重编码），但仍带「原始尺寸 → 缩小后尺寸」那行说明，让模型能把看到的坐标换算回原图。
5. **假设**本机图像处理能力不可用，**当**工具结果的图超预算，**则**入口这一步保持原始字节（不丢图、不阻断），出站沿用「无法在本机缩小时…」的降级路径，行为与改动前一致。
6. **假设**同一张图已经在入口降过，**当**它在后续多轮请求里反复出站，**则**不重复编码，输出字节保持稳定。
7. **假设**恢复一条旧会话，**当**它的消息里存着改造前的超预算原件，**则**照常可读、出站仍由那道闸兜住——入口收口只作用于之后新产生的工具结果，不回头改写历史消息。
8. **假设**当前模型不支持视觉，**当**工具结果带图，**则**维持既有行为（落盘 + 给路径、不内联发送），只是内存里那份同样降到预算内。
9. **假设**某张图在入口无法降下来（codec 抛错、格式不在白名单），**当**它被写进消息块，**则**保留原始字节并继续这一轮，不把整轮工具结果作废。

***

### 用户故事：出站图片按 2000 像素 / 5MB 预算收口（优先级：P1，对齐 Claude Code）

作为用户，我希望不管图片是从哪儿来的（粘贴、模型自己用 Read 工具读的本地图、工具产出的图、CLI 上以文件路径贴进来的图），只要它超出视觉模型能舒适接受的范围，都在发送前被缩小到安全范围，以便这一轮对话既不会因一张太大的图整轮失败，也不会白烧 token。

**这道闸现在是兜底**：工具产图与 Read 图片在新会话里已于入列表时降到预算内（见上一条故事），粘贴图在浏览器里就已收口，所以这里实际拦下的是历史消息、本机缩不动时留下来的图、以及直接以文件路径给出的图。它按「每一次出站」生效，因此不承担内存职责——内存由入口那一层管。

**为什么是这个优先级**：上限口径对齐 Claude Code——**单边 2000 像素**与 **base64 5MB**。其中 8192 像素是网关的硬边界：上游对**任一边超过 8192 像素**的图片直接返回 `HTTP 400 ... You have uploaded an unsupported image`（通用兜底文案，**不是**格式问题），并且**整轮请求失败**。实测（2026-09-20，`stream: true`，与客户端实际请求一致）：8192x1500 通过而 8193x1500 被拒、2250x8192 通过而 2250x9500 被拒；该边界只与单边像素有关——像素总数（28 MP 的 7000x4000 通过、16.5 MP 的 11000x1500 被拒）、字节数（6.8 MB 的 1500x1500 通过、1.3 MB 的 WebP 被拒）和容器格式都不影响判定。而 2000 像素 / 5MB 是我们**主动选的预算**：图更小则 token 更省、延迟更低，行为也与 Claude Code 一致；要特别注意 5MB **不是**网关的硬限（实测 9.0 MB base64 的 1500x1500 噪声图仍返回 200），它是我们主动设的体量预算。粘贴路径已经在浏览器里按 2000 收口（见「粘贴时在本地把图片降到 2000 像素预算」），所以这道 SDK 侧收口负责的是它覆盖不到的部分：**模型自己 Read 的本地图、工具产出的图、CLI 上以文件路径贴进来的图**（都没有浏览器能力），以及粘贴图中"尺寸已合规但体积超 5MB"的那一类。

**独立测试**：构造单边 3000 像素的图（尺寸超预算）与 base64 超过 5MB 的图（体积超预算）各一张，验证转换后的 API 请求里图片尺寸 ≤2000 像素、base64 ≤5MB 且容器格式仍在白名单内；构造 1200x800 的小图，验证送出的仍是原始字节。

**验收场景**：

1. **假设**出站图片任一边超过 2000 像素，**当**消息转换为 API 请求，**则**等比缩小到任一边不超过 2000 像素后再发送（长宽比不变）。
2. **假设**出站图片的 base64 超过 5MB，**当**消息转换为 API 请求，**则**先在不改变尺寸的前提下压缩，仍超则继续缩小尺寸，直到 base64 不超过 5MB。
3. **假设**出站图片的尺寸与体积都在预算内，**当**消息转换为 API 请求，**则**原样发送原始字节（不因"顺手压一下"而重编码）。
4. **假设**图片被缩小过，**当**消息转换为 API 请求，**则**附在图片后的尺寸说明同时给出原始尺寸与缩小后的尺寸，便于模型把看到的坐标换算回原图。
5. **假设**本机图像处理能力不可用（未安装、下载失败、加载报错），**当**消息转换为 API 请求，**则**不阻断这一轮：单边超过 8192 的图按「无法在本机缩小时…」那条处理，其余图片照常发送。
6. **假设**缩小与压缩之后仍超出预算（例如极端长宽比），**当**消息转换为 API 请求，**则**按同一条说明文本处理，不把超出硬边界的图发出去。
7. **假设**源图是 PNG / GIF / WebP 且带透明通道，**当**它被压缩或缩小，**则**不因处理而把透明区域变成黑块（优先选能保住透明的输出格式）。

***

### 用户故事：无法在本机缩小时，超出硬边界的图不进请求，改为一行说明（优先级：P1）

作为用户，我希望当本机没有可用的图像处理能力、缩不动那张超限图时，这一轮对话仍然能继续，并且模型/我能从消息里看出发生了什么以及该怎么做。

**为什么是这个优先级**：网关的 400 会让**整轮**请求失败。这条故事是「出站图片按 2000 像素 / 5MB 预算收口」在**本机缩不动**时的降级路径（正常路径见上一条）。已复现的触发条件与阈值：单边 > 8192 像素，与像素总数、字节数、格式无关。参照 opencode 的做法：跳过该图并在消息里留一行可操作的说明，比让整轮炸掉更有用。

**独立测试**：构造带超限尺寸图片块（数据 URL 与本地文件路径各一）的消息，验证转换后的 API 请求里没有该 `image_url`、而是出现包含实际尺寸与上限的说明文本，且同一条消息里的文本内容照常保留；构造预算内的图，验证仍原样发出。

**验收场景**：

1. **假设**本机图像处理能力不可用，且消息中的图片任一边超过 8192 像素，**当**消息转换为 API 请求，**则**不发送该图，改为插入一行文本说明（写明实际尺寸、8192 上限，以及"裁剪 / 分段 / 缩小后再试"的处理办法），消息中的其它内容照常发送。
2. **假设**图片任一边不超过 8192 像素，**当**消息转换为 API 请求，**则**原样发送（含体积超过 5MB 的图——此时没有压缩能力，宁可照发也不丢上下文）。
3. **假设**图片容器头解析不出尺寸（未知格式或头部被截断），**当**消息转换为 API 请求，**则**不因"判不出尺寸"而丢弃该图。
4. **假设**该图来自本地文件路径，**当**它因超限被跳过，**则**说明文本里带上该路径，便于模型裁剪后重新读取。

***

### 边界情况

* **如果剪贴板不包含图片会怎样？** 系统应忽略粘贴操作，或者如果同时存在文本则作为普通文本粘贴处理。
* **如果无法创建临时文件会怎样？** 系统应记录警告并不插入占位符。
* **如果用户从输入文本中删除了占位符会怎样？** 图片仍应在 `attachedImages` 列表中，但如果文本中未引用则不会发送（或者如果实现发送所有附加图片则可能会发送）。*注意：当前实现仅发送被引用的图片。*
* **如果用户粘贴非常大的图片会怎样？** 三层处理：粘贴路径先在本地按 **2000 像素预算**降采样（浏览器 canvas，零依赖、不依赖本机是否装上了图像库）；模型自己 Read 本地长截图、工具产出图这些没有浏览器能力的入口，在**进入消息列表那一刻**按同一套预算降到预算内（原件落盘、消息里带路径），这样 API 与展示流共用同一份小图；出站前再由 SDK 侧按 2000 像素 / 5MB 兜底一遍，拦住历史消息、本机缩不动的图与以文件路径贴进来的图。
* **上游硬边界（8192）和我们自己的预算（2000 / 5MB）是什么关系？** 两个不同的东西。8192 是网关的拒收边界，越过就整轮 400；2000 像素与 5MB 是我们主动选的预算，为的是省 token、降延迟并与 Claude Code 行为一致（5MB 甚至不是网关的硬限——实测 9.0 MB base64 的 1500x1500 噪声图仍返回 200）。**粘贴路径现在也按 2000 收口**：以前能原样通过的 2000~8192 像素粘贴截图，现在会在粘贴时就被缩小，长图上的小字会比以前更小；这是刻意取舍，而且预算调高调低都不涉及网关边界，可随时调整。
* **粘贴图还会带"原始尺寸 / 显示尺寸"那行说明吗？** 不会。那行说明只在图片**真的被缩过尺寸**时发出，而粘贴图在浏览器里已经缩到 ≤2000，SDK 侧看到的是预算内的图、原样放行。工具产图与 Read 图片的说明在入口降尺寸时就挂到那张图上了，之后每轮出站都带着它（出站不再重编码，但说明不丢）；直接以文件路径贴进来的图仍由出站那道闸在缩小后发出。
* **界面里点开看到的是原图还是降过的那份？** 降过的那份。消息列表里存的就是预算内的字节，所以聊天里的图片预览与超大图点击放大看到的都是缩放后的版本；需要原始分辨率时按消息里带出的路径打开原件。
* **本机图像处理能力拿不到时会怎样？** 不阻断对话：入口这一步保持原始字节，单边超过 8192 的图按「无法在本机缩小时…」跳过并留一行说明，其余图片照常发送（含体积超过 5MB 的图，宁可照发也不丢上下文）。这层的代价是：拿不到能力的环境里，超限图表现为"被跳过 + 说明"而不是"被缩小"，用户可按说明里的路径自行裁剪后重试。
* **本机图像处理能力是从哪来的、什么时候就绪？** 宿主自带的那份 CLI（桌面端 / VS Code / JetBrains 各自把 bundle 落到 `~/.wave/cli/<端>/`，身上没有 `node_modules`）在**启动时**按需从镜像站下载 sharp 及其运行期闭包（约 20MB，落到三端共享的 `~/.wave/cli/node_modules`，跨 CLI 升级保留），**准备好之前不对外提供会话服务**——所以不存在"刚启动就贴图、撞上还没装好"的窗口（与宿主先在启动期备好 ripgrep 再拉起 CLI 的做法一致）。用 npm 安装的 `wave-code` 自带该依赖，这一步是空转。装不上时只降级（见上一条），不影响聊天；同一进程内不重试（避免每贴一张图就打一次网络），**下次启动**会再试。
* **降采样/压缩后的输出体积也要有上限吗？** 要：base64 不超过 5MB；能保住透明就优先保住（PNG 类源图先试无损/调色板压缩），保不住才转 JPEG。
* **本地校验是结构体检而非完整解码**：头尾字节 + 宿主原生解码只能覆盖"空/明显不是图片/被截断/解码器解不开"这几类；一张结构完整但上游仍解不开的图仍可能通过，此时仍可能出现上游 400。
* **格式白名单是刻意收窄的**：svg / bmp / avif / heic 等即使宿主能显示也不放行（上游模型只接受 png/jpeg/gif/webp），被拒时按场景 2 提示用户转换格式。
* **拖拽/上传文件的图片走另一条链路**：桌面端拖入图片文件走"上传为附件 + 路径引用"，不会生成图片数据块，因此不在本校验范围内（本校验只作用于产生图片块的粘贴路径）；这类图片若尺寸超限，由「出站图片按 2000 像素 / 5MB 预算收口」与其降级路径兜底。
* **草稿里的图片存在哪里、存什么？** 只存在 webview 侧的本地存储（IndexedDB），按会话分键（新对话按 pane）；只存**已经能发送**的图片——粘贴中途失败、被格式/尺寸校验拒掉的图不入草稿；单会话保留张数有上限（当前定 20 张），超出时按最旧淘汰。图片字节**不进宿主保存的那份草稿字符串**：那段文本每次按键都会上报一次，把 base64 写进去等于每次输入都传 MB 级数据。
* **从草稿里删掉一张图会怎样？** 删除标签即从草稿里去掉该图——下次保存只登记还留在输入框里的图片，存储里那份随之下次保存清掉；整条草稿发出去之后，该会话的草稿图片整体清空。
* **草稿图片和「发送前校验图片有效性」是什么关系？** 同一道校验：图片先过校验（确认能发送）才登记进草稿，所以回填出来的图片不会等到发送时才被拒。
