Skip to content

功能规格说明:图片粘贴 ​

创建日期: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. 假设 消息中有多个图片占位符,当 用户提交消息,则 所有引用的图片都包含在消息载荷中。

用户故事:跨平台支持(优先级: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. 假设 图片没有本地文件路径(如 webview 通过 stdio 传入的 dataURL),当 CLI 收到图片并将消息转换为 API 格式时,则 先将图片落盘为临时文件,再在图片块后附加文本 [Image source: <落盘路径>]
  3. 假设 图片既没有本地文件路径也无法落盘,当 消息转换为 API 格式时,则 不附加元数据文本,仅发送图片

用户故事:发送前校验图片有效性(优先级: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。

用户故事:出站图片按 2000 像素 / 5MB 预算收口(优先级:P1,对齐 Claude Code) ​

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

为什么是这个优先级:上限口径对齐 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,零依赖、不依赖本机是否装上了图像库);发送前再由 SDK 侧按 2000 像素 / 5MB 预算统一收口。模型自己 Read 本地长截图、工具产出图、CLI 文件路径这些入口没有浏览器能力,由 SDK 侧那一层包办。
  • 上游硬边界(8192)和我们自己的预算(2000 / 5MB)是什么关系? 两个不同的东西。8192 是网关的拒收边界,越过就整轮 400;2000 像素与 5MB 是我们主动选的预算,为的是省 token、降延迟并与 Claude Code 行为一致(5MB 甚至不是网关的硬限——实测 9.0 MB base64 的 1500x1500 噪声图仍返回 200)。粘贴路径现在也按 2000 收口:以前能原样通过的 2000~8192 像素粘贴截图,现在会在粘贴时就被缩小,长图上的小字会比以前更小;这是刻意取舍,而且预算调高调低都不涉及网关边界,可随时调整。
  • 粘贴图还会带"原始尺寸 / 显示尺寸"那行说明吗? 不会。那行说明由「出站图片按 2000 像素 / 5MB 预算收口」在它真的缩过尺寸时发出,而粘贴图在浏览器里已经缩到 ≤2000,SDK 侧看到的是预算内的图、原样放行。只有 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 预算收口」与其降级路径兜底。