Skip to content

fix: 复制图片改由本机服务端同步写剪贴板(复制后立刻可粘) - #67

Merged
zlZayn merged 3 commits into
mainfrom
feat/clipboard-server-write
Nov 1, 2026
Merged

zlZayn merged 3 commits into
mainfrom
feat/clipboard-server-write

Conversation

@zlZayn

@zlZayn zlZayn commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator

问题

预览大图里点「复制图片」,界面立刻显示「已复制图片」,切到微信 Ctrl+V 什么都没出来;多按几次偶尔成功;停在页面上等 1 秒左右再粘就稳定。

真机实测(有头 Chrome、127.0.0.1 安全上下文、复刻 copyImageToClipboard 全链路):

观测点 结果
navigator.clipboard.write() resolve 2.6 MB PNG 约 30~40 ms
resolve 那一刻,外部进程读系统剪贴板 image=NONE(还没有位图)
多久之后才读得到 约 300 ms 后
合成 4.2 MB 图那轮 write() 成功返回,剪贴板仍是上一张(整次静默丢弃)
12 MB JPEG 走 canvas 转码 转码 611 ms,产出 17 MB PNG

也就是说:浏览器写图片是异步投递,Promise 的 resolve 只代表「请求已受理」。所以「时间」不是关键变量,写有没有真的提交才是——这也解释了「切走再等两秒也不行」(页面失活时提交被挂起)。

改法

分两层,一改一兜底。

① 根因:复制的可信来源改成后端。 应用本来就在本机跑,让 Python 直接写剪贴板:

  • 新增 core/clipboard.py:Win32 SetClipboardData(CF_DIB),同步——返回即别的程序已能粘到(实测 275 ms 往返;16 MB 大图 529 ms)
  • 新增 POST /api/clipboard/image:文件不存在 404,写不了(非 Windows / 被别的程序占着)503,前端据此静默退回浏览器路径
  • CF_DIB 自己拼字节而非「存 BMP 再切头」:40 字节 BITMAPINFOHEADER + 24 位 BGR + 自下而上 + 每行 4 字节对齐。这三条写错都不报错、只是图坏掉,因此全部进单测
  • OpenClipboard 被别的程序(剪贴板管理器 / IM)占着时返回 0(err 5)→ 短暂重试,不当失败

② UI:不再拿 write() 的 resolve 当成功。

  • 反馈三态:复制中… → 已复制 / 失败,落地后才说「已复制」
  • 进行中禁用两颗复制按钮——连点会并发写剪贴板,最后落地哪张不确定
  • 有本机绝对路径走服务端;没有路径(外链 / blob / data URL)或服务端不可用 → 退回浏览器 API,功能不因后端缺席而消失

验证

  • 后端 482 passed;前端 1066 passed;tsc --noEmit / eslint / ruff check / ruff format --check 全绿
  • E2E verify_canvas.py:105/105 PASS(隔离实例)
  • 真机端到端:接口返回后立刻用另一个进程读剪贴板 → image=1254x1254(不再是 NONE);真实 UI 里点「生成历史 → 双击图片 → 复制图片」→ POST /api/clipboard/image -> 200,剪贴板当场换成新图
  • python scripts/check_docs.py 通过(链接 541 处 + 计数一致)

影响与边界

  • 新增 pillow 运行时依赖(uv.lock 已更新,CI 用 uv sync --locked)
  • 该能力仅 Windows;非 Windows 仍走浏览器路径,那条的时间差还在(已知降级,不影响功能)
  • 服务端进程退出后这份剪贴板数据会失效(应用在跑就没问题)
  • 设计背景与被否方案(回读校验要弹权限 / 前端发原始像素 / PowerShell 转码 / 只写 PNG 格式 / 只改文案)记在 .agents/notes/2026-11-01-clipboard-write-by-server.md

提交拆分

  1. feat(clipboard) 后端能力 + 两个测试文件
  2. fix(preview) 前端接线 + 三态反馈 + 前端测试
  3. docs 契约(7.1)、防错条(9.4 第 14 条)、子 README、仪表盘计数、决策记录

浏览器 navigator.clipboard.write() 写图片是异步投递:resolve 时系统剪贴板里
还没有位图,实测要 100~300ms 后才读得到(大图更久,偶发整次丢弃,页面失活还
会被挂起)。于是「已复制」的提示比「能粘」早一截——用户点完切到微信立刻 Ctrl+V
拿到的是空的,多按几次偶尔成功。

改为由本机后端同步写:POST /api/clipboard/image -> core/clipboard.py 用 Win32
SetClipboardData(CF_DIB),接口返回即代表别的程序已经能粘到(实测 275ms 往返)。

CF_DIB 自己拼字节而不是存 BMP 再切头:40 字节 BITMAPINFOHEADER + 24 位 BGR +
自下而上 + 每行 4 字节对齐,三条写错都不报错、只是图坏掉,因此进单测。
- imageCopy:一条门面收口两条路。有本机绝对路径先走 POST /api/clipboard/image,
  没有路径(外链 / blob / data URL)或后端不可用(非 Windows / 旧服务端)再退回
  浏览器 API,功能不因后端缺席而消失
- ZoomModal:反馈从「已复制 / 失败」两态改为「复制中… → 已复制 / 失败」,进行中
  禁用两颗复制按钮(连点会并发写剪贴板,最后落地哪张不确定)
- 新增 imageCopy.test.ts 锁路径决策;WorkflowModals.test.tsx 补「先复制中、后已复制
  且进行中禁用」回归
- ARCHITECTURE 7.1 加 POST /api/clipboard/image;8.3 记复制的落地口径;
  9.4 新增第 14 条(「已复制」比「能粘」早:现象 / 实测数据 / 规范 / 判据)
- core/README 加 clipboard.py 节(导出、依赖、改后必测、两条边界)
- frontend/README 更新 imageCopy.ts 与 WorkflowModals 条目、api.ts 依赖
- tests/README 登记两个新测试文件 + imageCopy.test.ts,计数 482 / 1066
- 新增决策记录 2026-11-01-clipboard-write-by-server(含被否方案)
@zlZayn
zlZayn merged commit 23090ff into main Nov 1, 2026
4 checks passed
@zlZayn
zlZayn deleted the feat/clipboard-server-write branch November 1, 2026 06:44
zlZayn added a commit that referenced this pull request Nov 3, 2026
只写 24 位 CF_DIB 时 alpha 被整条丢掉:透明背景的 PNG 粘出来透明区是一片黑
(透明像素 RGB 通常是 0,转 RGB 只是把 alpha 扔掉)。浏览器旧的复制路径本来就
同时提供 PNG 与 DIB,所以这属于 #67 引入的功能退化。

- 剪贴板同时写两种格式:CF_DIB(24 位,给微信/画图/老软件)+ 注册格式 PNG
  (源是 PNG 用原字节、其它转 PNG,alpha 完整,给 Photoshop/Office/浏览器)
- DIB 的透明像素改为合成到白底(不再是丢弃后留原 RGB 的黑块)
- 单测:透明合成白底、PNG 载荷原字节直传且 alpha 完好、非 PNG 源转 PNG

实测(写剪贴板后回读):格式含 PNG;PNG 透明区 alpha=0、不透明区 255;
DIB 透明区为白。
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.

1 participant