dsh-longpress-shot
Long-press the left mouse button anywhere in DSH, drag a box, and the region lands on disk as a PNG — with a preview card that drops the image straight into the composer.
dsh-longpress-shot adds region capture to DeepSeek Harness without a hotkey, a tray menu, or a separate app. Hold the left button for half a second, the page dims and turns into a crosshair, drag the area you want, release. The crop is written to ~/.dsh/longpress-shots/ and the same bytes go into the system clipboard.
Windows only (the capture path is GDI through PowerShell).

Install
dsh plugin add dsh-longpress-shot
Or install straight from the release tarball, which needs no registry:
dsh plugin add "https://github.com/kkkkk-tubu/dsh-longpress-shot/releases/download/v1.11.2/dsh-longpress-shot-1.11.2.tgz"
Then restart DSH. Nothing appears in the navigation bar — the plugin works everywhere, on any page, with no entry point to find.
Manual install: put the package under <profile>/node_modules/, add it to the profile's dependencies, restart.
Usage
| Action | Result |
|---|---|
| Hold left button ≥ 550 ms | Page dims, cursor turns into a crosshair |
| Keep holding, then drag | Blue selection box follows the pointer; release captures that region |
| Hold and release without dragging | Stays armed — press again to redraw the box (never silently captures the whole window) |
Double-click or Enter while armed |
Capture the whole DSH window |
Right-click or Esc |
Cancel |
The crop is saved as ~/.dsh/longpress-shots/shot-YYYYMMDD-HHMMSS-mmm.png, copied to the system clipboard, and shown in a preview card with 发到输入框 (send to composer) / 另存为 (save as) / 复制路径 (copy path) / 关闭 (close).
To get the image into a conversation, use the card button, press Ctrl+V in the composer, or drag the thumbnail onto it — all three end up in the same attachment intake.
Drag the card by its header to move it; the position is remembered in localStorage. The default position avoids the whale widget in the bottom-right corner.
The sections below are the full engineering notes: how the capture path works, why the drop path falls back to a synthetic paste, how to run the self-tests, and how to roll back.
1. 交互细节(中文工程笔记)

| 操作 | 结果 |
|---|---|
| 左键按住不动 ≥ 550ms | 出现半透明遮罩,鼠标变十字 |
| 长按后不松手继续拖 | 实时画蓝色选框,松手 → 抓该区域 |
| 长按后直接松手 | 停在遮罩里,可再按下重新拖框(不会误抓整窗) |
遮罩里双击 / 按 Enter |
抓整个 DSH 窗口 |
右键 / 按 Esc |
取消,什么都不抓 |
抓完弹预览卡:缩略图 + 像素尺寸 + 文件名,四个按钮 发到输入框 / 另存为 / 复制路径 / 关闭。
图片同时写进系统剪贴板,Ctrl+V 也可直接贴进对话框。

把截图送进对话框,有三种方式(按可靠度排序):
| 方式 | 操作 | 说明 |
|---|---|---|
| 按钮(推荐) | 点卡片上的 发到输入框 |
直接把图作为附件插进输入框,立刻出现附件缩略图 |
| 粘贴 | 点一下输入框,Ctrl+V |
抓图时已写入系统剪贴板 |
| 拖拽 | 按住卡片缩略图拖到输入框 | 能拖就直连;Chromium 吞掉 Files 时由 drop 兜底改走粘贴通道 |
预览卡怎么摆弄:
| 操作 | 结果 |
|---|---|
| 按住卡片标题栏拖 | 卡片跟着鼠标走,松手后位置写进 localStorage,下次开在同一处 |
| 拖出窗口 / 拖到边缘 | 夹在视口内,留 8px 边距,不会被拖丢 |
| 点图片区或按钮区 | 不触发拖动(只有 header 是把手) |
点 × 或 关闭 |
关卡片;点在卡片上的任何事件都不会漏给桌宠 |
默认位置:视口右下角往左上偏移,避开鲸鱼挂件的矩形(右下角那只会说话的鲸鱼)。
鲸鱼位置从 .dshwv-root 的 getBoundingClientRect() 现算;找不到就退回右下角。
不触发的场景:input / textarea / select / contenteditable 内长按(不抢文本选择)。
把缩略图拖进输入框,附件缩略图立刻出现在 composer 里:

2. 目录结构
dsh-longpress-shot/
├── package.json # dsh.bundle.patch → cordis.patch.yml
├── cordis.patch.yml # 插件注册补丁
├── lib/
│ ├── index.js # 宿主半区:注入行 + 四条路由 + 调 PowerShell
│ ├── shot.js # 浏览器半区:长按检测、遮罩框选、预览卡
│ └── capture.ps1 # GDI 抓窗口客户区 + 按比例裁剪 + 写剪贴板
├── screenshots/ # README 用图(真机截的,1100×706)
├── screenshots.json # 图片清单,市场页读它
├── selftest-drag.mjs # 离线自测:假 DOM 跑完整交互状态机(9 组,全 PASS)
├── selftest-row.mjs # 离线自测:注入行 DESKTOP_ROW_TEXT 的解析与行为(18 项,全 PASS)
├── selftest-card.mjs # 离线自测:预览卡拖动 / 默认位置 / 事件隔离 / 送进对话框(42 项,全 PASS)
├── selftest-dragout.mjs # 离线自测:拖缩略图出卡片 + drop 兜底(32 项,全 PASS)
├── mouse-sim.ps1 # 真机鼠标模拟:longpress / drag / click,物理像素
├── validate-profile.mjs # 校验 profile 注册是否完整
└── sync-to-profile.sh # 源码 → profile 同步(见第 4 节)
3. 工作原理
页面 pointerdown ──550ms──▶ shot.js 建遮罩(armed 继承按住这一笔)
│ │
│ pointermove → paint() 画选框
│ │
└──── pointerup ─────────┴─▶ POST /dsh-longpress-shot/capture
{x,y,w,h,aspect} 比例,非像素
│
lib/index.js 夹紧到 [0,1]
│
spawn powershell capture.ps1
│
EnumWindows 找 DSH 窗口 → 客户区坐标
CopyFromScreen → Clone(裁剪矩形) → Save PNG
│
{ok, file, width, height, clientW, ...}
│
shot.js 弹预览卡 + 写剪贴板
│
┌──── 缩略图拖到输入框 ────┐ └── 点「发到输入框」──┐
dragstart 塞 File 宿主 drop 拿不到 Files 合成 ClipboardEvent('paste')
│ │(Chromium 同页 DOM 拖拽 clipboardData.items=[File]
└───────────────────────▶│ 不暴露 File) │
└──▶ pasteIntoComposer() ◀─┘
│
宿主 Lexical PASTE_COMMAND 读 items
│
handlers.intakeFiles(files) → 附件缩略图
为什么抓屏在宿主做:页面拿不到 OS 级截图(getDisplayMedia 会弹选择器)。
宿主插件跑在 Node 里,可以直接起 PowerShell 用 GDI 抓窗口客户区。
为什么注入用内联 script 而不是 script-src 行:桌面端 index.html 是安装包静态直出,
tapIndex 变换不生效;而 script-src 行加载失败会 reject 掉整个 __DSH_BOOT_READY__,
把 DSH 界面弄瞎。内联脚本自己 onerror 吞掉,路由不在时静默失败。
为什么选区是比例不是像素:capture.ps1 先 SetProcessDpiAwarenessContext(-4)(per-monitor v2),
之后全是物理像素;前端只知道自己 CSS 像素。传比例,两端各自换算,缩放/DPI 都能对上。
为什么送进对话框走粘贴而不是拖放:同页面 DOM 拖拽时,Chromium 只把 text/* 与
chromium/x-drag-id 交给 target,DataTransferItem 里的 File 到 dragenter 就没了
(dragstart 里 types='Files'、drop 里 types='text/plain|chromium/x-drag-id',真机 diag 逐条对上)。
宿主 installDocumentDropEvents 判 dt.types.includes("Files"),恒假即放弃。
而 PASTE_COMMAND 只读 event.clipboardData.items,不经过 OS 剪贴板 —— 合成一个
ClipboardEvent 就能直接喂给 intakeFiles。这条是唯一稳的路,别改回拖放。
4. 改完代码怎么让它生效
关键:宿主读的是 profile 里那份 lib/shot.js,不是 workspace 里这份。
两者原本是硬链接,但 edit/write 是「写临时文件再改名」,inode 会换,链接就断了。
所以每次改完源码跑:
cd "C:/Users/19906/Documents/deepseek-harness/default-workspace/dsh-longpress-shot"
bash sync-to-profile.sh
脚本会判断是否已是同一 inode,是就跳过,不是就复制。
| 改了什么 | 怎么生效 |
|---|---|
lib/shot.js |
sync-to-profile.sh 后不用手动刷新:loader 每 5s 轮询 /dsh-longpress-shot/version,发现磁盘版本 ≠ 页面版本就自己 location.reload();最多等 5–8 秒 |
lib/capture.ps1 |
同上,每次抓屏都是新起进程 |
lib/index.js / package.json / cordis.patch.yml |
sync-to-profile.sh 后必须重启 DSH(注入行和路由是启动时注册的) |
打包版没有 Ctrl+R
E:/DSH/resources/app/lib/main.js 里 role: "reload" 的菜单项包在 development 分支里,
打包版根本没绑 Ctrl+R。所以让新 shot.js 进页面的手段只有两条:
- 自动(默认走这条):loader 每 5s 轮询版本,改完
bash sync-to-profile.sh等 5–8 秒自己刷。 - 手动:
F12打开 DevTools(toggleDevTools没被 development 门控,一定可用), 在 DevTools 窗口里按Ctrl+R;或托盘右键 → 退出,重开 DSH。
两种生效等级,别混
| 目标 | 需要什么 | 为什么 |
|---|---|---|
新 shot.js 进页面 |
刷新即可 | GET /dsh-longpress-shot/shot.js 按 mtime 重读 + no-store,每次请求都读磁盘 |
/version 路由 + loader 轮询 |
重启一次 DSH | 这两样是 index.js 的 apply() 里注册的,只在宿主启动时执行一次 |
所以「框选修复」单靠刷新就能生效;「版本自愈 loader」必须重启一次才能装进注入表。 判断当前进程有没有 loader:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:63342/dsh-longpress-shot/version
# 200 = 有;404 = 进程还在用旧 index.js,重启一次
验证当前路由喂的是新版:
curl -s http://127.0.0.1:63342/dsh-longpress-shot/version
# {"ok":true,"version":"1.8.0","bytes":29799}
curl -s http://127.0.0.1:63342/dsh-longpress-shot/shot.js | grep -c "version: '1.8.0'"
# 1
端口从运行时上下文拿(当前是 63342)。
5. 自测
cd "C:/Users/19906/Documents/deepseek-harness/default-workspace/dsh-longpress-shot"
ELECTRON_RUN_AS_NODE=1 "E:/DSH/DeepSeek Harness.exe" --check lib/shot.js # 语法
ELECTRON_RUN_AS_NODE=1 "E:/DSH/DeepSeek Harness.exe" selftest-drag.mjs # 交互状态机
ELECTRON_RUN_AS_NODE=1 "E:/DSH/DeepSeek Harness.exe" selftest-row.mjs # 注入行本身
ELECTRON_RUN_AS_NODE=1 "E:/DSH/DeepSeek Harness.exe" selftest-card.mjs # 预览卡
真机鼠标模拟(会真的动你的鼠标,物理像素):
# 长按 (1000,400) 0.9s 后拖到 (1400,700) 松手 → 抓框
powershell -ExecutionPolicy Bypass -NoProfile -File mouse-sim.ps1 `
-Action longpress -X1 1000 -Y1 400 -X2 1400 -Y2 700
# 在 (X1,Y1) 按住拖到 (X2,Y2) → 测卡片拖动
powershell -ExecutionPolicy Bypass -NoProfile -File mouse-sim.ps1 `
-Action drag -X1 1959 -Y1 305 -X2 1559 -Y2 355
# 单击
powershell -ExecutionPolicy Bypass -NoProfile -File mouse-sim.ps1 -Action click -X1 1835 -Y1 1147
mouse-sim.ps1 只选 DeepSeek Harness 那个有标题的窗口做前台。
注意系统禁用了脚本执行时要用 powershell -ExecutionPolicy Bypass -File(不能用 & .\x.ps1)。
这台机器 PATH 里没有 node/npm/npx,所以要用 Electron 自带的 Node
(ELECTRON_RUN_AS_NODE=1 + exe 路径)来跑 .mjs。
selftest-drag.mjs 覆盖 9 组:
- 长按 600ms 触发遮罩
- 不松手继续拖 → 选框尺寸正确
- 松手 → 按选区提交(不再是整窗)
- 长按后直接松手 → 不抓图,留在遮罩
- 遮罩里重新按下拖框 → 正常出选区
Esc→ 取消不抓图input上长按不触发- 遮罩里双击 → 整窗;第一次按下不能误判成双击
Enter→ 整窗
selftest-card.mjs 覆盖 42 项,专测预览卡:
- 卡片创建
- 默认位置不与鲸鱼矩形相交、在视口内、走
left/top内联定位、right/bottom清成auto - header 拖动生效(位移量精确匹配)、图片区按下不拖动、位置写
localStorage、松手清_lps-dragging ×的click在 window 捕获层被stopImmediatePropagation截断、真的关掉卡片- 拖完松手浏览器补发的那个
click不触发按钮 - 卡片外的
click/pointerdown不受影响(不误伤页面) - 再抓一次,卡片落在
localStorage记的位置 - 拖源是
div._lps-drag不是img;dataTransfer里有 1 个真File、名字是shot-*.png、没有DownloadURL、没有text/uri-list [10]点发到输入框→ 合成ClipboardEvent把文件递给 composer(假 composer 读items[i].getAsFile(),断言名字与 MIME)[11]复现真机现象:dragover时types里没有Files,宿主放弃 → 我们的drop兜底自动改走粘贴;dragend不重复插入[12]拖到非输入框区域不误插
为什么必须挂在 window 捕获而不是 document:鲸鱼挂件(dsh-whale-widget)的
onDocClickStopper / onDocPointerDown 挂在 document 捕获,判定用的是
isWhaleHit() 的几何命中(拿 img 的 rect + 透明像素画布算,完全不看 e.target)。
卡片压在鲸鱼身上时它照样认为「点在鲸鱼上」→ 吞掉 click,表现就是「点关闭结果点到桌宠」。
捕获路径 window → document → … → target,我们必须在 window 上先跑。
为什么拖动结束要设 dragSuppressOnce:拖拽松手后浏览器会补发一次 click,
它的 target 是拖动终点下面的元素。若终点压在 × 上就会误关卡片。
真实点击一定从 pointerdown 开始,所以 pointerdown 时清标记、补发的 click 吃掉。
selftest-row.mjs 覆盖 18 项,专治「宿主全 200、页面完全没反应」这一类:
括号/可解析、SCRIPT_URL/VERSION_URL 拼接、() {} 计数平衡,再在 node:vm
假环境里真跑一遍 IIFE,逐项断言真的挂了 <script>、真带了 ?v=、真注册了 5s 轮询、
真发了一次 probe、window 上真设了 __DSH_LONGPRESS_LOADER__、第二次执行不再挂。
这些断言必须逐项做:注入行整段包在 try{}catch(e){} 里,任何 ReferenceError
都会被静默吞掉,「执行不抛异常」本身完全不构成证据。
后端裁剪链路的单独验证(绕开前端):
curl -s -X POST http://127.0.0.1:51916/dsh-longpress-shot/capture \
-H "Content-Type: application/json" \
--data-binary '{"x":0.25,"y":0.20,"w":0.40,"h":0.35,"aspect":1.6,"clipboard":false}'
期望 width/height 约为 clientW*0.40 × clientH*0.35(不是整窗)。端口取
C:/Users/19906/.dsh/dsh-config-manager/running/desktop.json 里的 port,重启会变。
6. 排错
| 现象 | 原因 / 处理 |
|---|---|
| 长按没反应 | 页面没刷新;或焦点在输入框里;curl .../shot.js 看路由是否 200 |
| 截图尺寸比屏幕上小一圈(约 67%) | capture.ps1 的 DPI 感知没生效 → PowerShell 以「虚拟化」跑,GetClientRect 返回 ÷1.5 的尺寸。查两处:SetProcessDpiAwarenessContext 声明返回 bool(不是 IntPtr)、文件带 UTF-8 BOM。selftest-drag.mjs 会打印 clientW/clientH |
改了 capture.ps1 但行为没变 |
BOM 被编辑器/同步脚本剥掉了。xxd -l 3 lib/capture.ps1 必须是 efbbbf |
| 蓝框几乎看不见 | v1.7.0 前蓝框没有 z-index,被遮罩压暗后与背景只差约 18% 亮度。升级 shot.js 即可 |
长按完全没反应,但三条路由全 200、/version 也正常 |
注入行 DESKTOP_ROW_TEXT 解析失败,整段被浏览器丢弃。跑 selftest-row.mjs 定位;2026-10-26 就是这么挂的(多一个 }) |
| 一直是整窗截图 | 用的是 v1.0.0 的 shot.js(没有 armed 逻辑)。跑 sync-to-profile.sh,等 5–8 秒自动刷新(或 F12 → DevTools 里 Ctrl+R) |
Ctrl+R 按了没用 |
打包版没绑这个键(main.js 里 reload 菜单项在 development 分支)。改用版本自愈轮询、F12 → DevTools 内 Ctrl+R、或退出重开 |
curl .../version 返回 404 |
进程还在跑旧 index.js,/version 路由没注册。重启一次 DSH |
| 改完 shot.js 一直不自动刷新 | /version 404(同上),或页面开了多个窗口只有当前窗口有 loader |
抓屏失败:window-not-found |
EnumWindows 没匹配到客户区 ≥200×200 的 DSH 窗口,检查窗口是否最小化 |
抓屏失败:empty-region |
选框太小(<8px)或比例算出来 <1px |
抓屏失败:timeout |
PowerShell 15s 没回,通常是被杀软拦了 |
| 提示「窗口宽高比与页面不一致」 | aspectMismatch:抓到的客户区宽高比和页面视口差 >8%,说明抓错了窗口 |
| 预览卡拖不动 | 只有 header 是把手;按在图片或按钮上不拖。selftest-card.mjs 第 3 组能定位 |
| 点关闭结果点了桌宠 | 事件隔离没挂到 window 捕获。鲸鱼用 document 捕获 + 几何命中,window 上才抢先。selftest-card.mjs 第 4 组 |
| 拖完松手卡片自己关了 | dragSuppressOnce 逻辑错:补发的 click 被当成真点击。selftest-card.mjs 第 4b 组 |
| 拖框经过预览卡后框不动、松手没截图 | v1.8.0 前遮罩 z-index(2147483000)低于预览卡(2147483001),指针移进卡片就被 window 捕获的 onShield 截断。升级 shot.js(/version 应为 1.11.2) |
| 拖缩略图到输入框什么都不发生 | 先看 diag.jsonl 的 dragenter 有没有 "over":true。没有 = 落点判定没命中(v1.11.2 修的就是这个:ev.target 是 composer 外层容器,ed.contains(t) 恒假)。有 over:true 却没插进去,再看 tag=paste-inject 的 consumed 是否为 true |
点 发到输入框 提示「没找到输入框」 |
composerEl() 只认 .cm-content[contenteditable] 等可编辑区;先点一下对话输入区让 composer 挂载 |
| 预览卡盖住鲸鱼气泡 | 默认位置会避开 .dshwv-root 的矩形;鲸鱼挪了位置要等下次开卡才重算 |
排查顺序(自上而下,每步都能独立证伪一环):
curl -s http://127.0.0.1:<port>/dsh-longpress-shot/version—— 404 说明index.js没加载,先去重启,别往下查。ELECTRON_RUN_AS_NODE=1 ... selftest-row.mjs—— 注入行本身有没有解析错。curl -s .../shot.js | wc -c—— 路由给的是不是当前版本(v1.11.2 = 44099 字节)。- 页面里
window.__DSH_LONGPRESS_LOADER__/window.__DSH_LONGPRESS_SHOT__—— 前者 1 说明注入行跑了,后者存在说明 shot.js 加载成功。两个都没有但 1、2、3 都正常 → 注入表没进这个窗口(桌面端注入表在宿主启动时一次性收集,重开一次窗口即可)。
7. 回退
只想关掉但保留文件:从 profile 的 package.json 里删掉 dsh-longpress-shot 依赖,
或用插件管理页停用它,然后重启 DSH。
完全卸载:
cd "C:/Users/19906/.dsh/profiles/desktop"
# 1) 从 package.json 的 dependencies 里删掉 "dsh-longpress-shot"
# 2) 删包
rm -rf node_modules/dsh-longpress-shot
# 3) 重启 DSH
代码改坏了想退回:改的是 workspace 里那份,profile 那份是副本 —— 直接
git checkout(如果 workspace 在版本控制下)或从备份恢复,再跑 sync-to-profile.sh。
反过来,如果 profile 那份坏了而 workspace 是好的,跑一次 sync-to-profile.sh 就修回来。
注意:profile package.json 里那行 dsh-longpress-shot 依赖一旦删掉,
下次 pnpm install 就会把包清掉;只删 node_modules 里的目录而不删依赖行,pnpm install 会重新装回来。
8. 安全边界
三条路由都过 rejected():
- Host 必须是回环地址(
localhost/127.x.x.x/::1),挡 DNS 重绑定 Sec-Fetch-Site: cross-site一律 403- 有
Origin时其 host 必须与 Host 一致
/file 只接受正则 ^shot-[0-9]{8}-[0-9]{6}-[0-9]{3}\.png$ 且 basename(name) === name,挡路径穿越。
capture.ps1 只抓本进程祖先链上的窗口(从 -AncestorPid 向上最多 10 层),不是全屏任取。
9. 版本
v1.11.2(当前)— 修 v1.11.0 遗留的「有的拖法还是没反应」:
现象:v1.11.0 上线后,点
发到输入框一定成功,但用手指拖有时成功、有时什么都不发生。查
diag.jsonl看到关键一列:dragover里over=False,而落点明明在输入框上。 根因在inComposer():// 旧(错) return ed === t || ed.contains(t) || t.closest('.cm-content,...')drop/dragover的ev.target是 composer 的外层容器 (实测依次是DIV.an3WzG_composerStack→DIV.GRgSLq_card→DIV.GRgSLq_input→DIV.GRgSLq_row→BUTTON.GRgSLq_add),这些是.cm-content的祖先, 所以ed.contains(t)恒假、t.closest('.cm-content')也恒 null →overComposer永远是false→rescue()第一道判据就 return → 拖拽不插文件。反过来只要指针恰好扫到编辑器本体(.cm-content),就会成功—— 这就是「时好时坏」的来源。改动(
lib/shot.js):inComposer(t, ev)三个方向都认:ed.contains(t)/t.contains(ed)/t.closest('.cm-content,[data-lexical-editor="true"],.cm-editor')。- 加几何兜底:指针
clientX/clientY落在编辑区矩形外扩一圈 (左右 40px、上 70px、下 30px,覆盖工具条与内边距)内也算命中。 dragenter也参与粘住overComposer(原来只有dragover更新它), 并用overComposer || inComposer(...)累积而不是覆盖——拖拽途中划出去一次不会清零。rescue()判据改成overComposer || inComposer(ev.target, ev),dragend的ev.target是拖源自己(DIV._lps-drag),靠坐标兜底也能命中。diag增列:dragenter.over、drop-rescue.over/near。
真机验收(2026-10-28 18:35,客户区 1919×1232 origin
(320,185)): 拖拽路径(1160,930) → (760,1100),diag序列为dragenter ... over=True→drop(types无Files)→paste-inject {"consumed":true,"dispatched":false}→drop-rescue {"ok":true,"over":true,"near":true}→ 输入框里出现附件缩略图 + 光标。 全程不再弹任何提示(v1.11.1 已把兜底提示删掉)。自测:
selftest-card.mjs 42 项/selftest-drag.mjs/selftest-dragout.mjs 32 项/selftest-row.mjs 18 项全 PASS。v1.11.1 — 删掉拖拽兜底时的 toast(用户反馈「只要拖进去了就行,不用出这个提示」)。
v1.11.0 — 修「之前能把截图拖到对话框,现在不行了」:
现象:卡片缩略图拖到输入框,松手什么都不发生;
diag.jsonl里drop事件明明有。查下来是拖拽数据在 target 侧就没了(真机 diag 逐条记录):
dragstart时types='Files'、nfiles=1、名字shot-*.png—— 我们的真File在。- 一到
dragenter/dragover/drop,types只剩text/plain|chromium/x-drag-id、nfiles=0。Chromium 对同页面 DOM 拖拽不会把DataTransferItem的 File 暴露给 target。 - 宿主
@deepseek-ai/dsh-client-ui-attachment/lib/client.js的installDocumentDropEvents里dt.types.includes("Files")恒假 →onDrop直接 return。
换成粘贴通道就通了。宿主的 composer 是 Lexical,粘贴走
dsh-client-ui-conversation/lib/client.js里Je$2(PASTE_COMMAND)那条命令, 只读event.clipboardData.items:kind==='file'→getAsFile()→handlers.intakeFiles(files)。 这条路不碰 OS 剪贴板,所以手造ClipboardEvent就能喂进去 (new ClipboardEvent('paste', { clipboardData: dt }),Chromium 支持 ClipboardEventInit 带 clipboardData)。改动(
lib/shot.js):- 新增
pasteIntoComposer(file):new DataTransfer()→items.add(file)→ 在.cm-content上派发paste。dispatchEvent返回 false(宿主 preventDefault)= 已被消费。 - 新增
sendCardToComposer()+ 卡片按钮发到输入框。 drop/dragend兜底:拖到输入框上但宿主因没有Files放弃时,自己走粘贴,dropRescued防重复插入。- 拖源仍是
div._lps-drag(v1.10.0),img保持draggable=false。
selftest-card.mjs扩到 42 项(新增[10][11][12]三组:按钮派发 paste、drop 兜底、落空白处不误插),全 PASS。真机验收(2026-10-28 15:40,客户区 1919×1232 origin
(320,185),window hwnd525296):- 长按拖框
(640,300)→(1000,560)→ 落盘shot-20261028-154437-293.png360×260,1:1。 - 点
发到输入框→diag记paste-inject {"consumed":true,"dispatched":false}, 输入框上方出现该截图的附件缩略图。 - 拖到输入框松手 →
drop(types无Files)→ 同一笔自动走paste-inject→ 附件出现。
v1.10.0 — 换拖源:
img原生拖拽会把自己的File顶成 Chromium 虚拟文件file.png(名字取自 URL 末段/dsh-longpress-shot/file),读不出字节。改成外层div._lps-drag当拖源、img设draggable=false;onCardDragStart只做dt.items.add(file), 删掉DownloadURL与text/uri-list。v1.9.x — 预览卡缩略图恢复可拖、加拖放诊断(
diag.jsonl)。v1.8.0 — 修「拖框经过预览卡时框不动、松手没截图」:
现象:长按触发遮罩后,把框拖到右侧预览卡(「截图已保存」那张)覆盖的区域, 蓝框就停住不跟随;继续拖到卡片外面也不恢复,松手也抓不出图。
原因不是 todo 面板、不是
pointermove断流,而是预览卡把指针事件吃掉了:._lps-overlay是z-index:2147483000,预览卡是2147483001—— 卡片在遮罩上面。 指针移进卡片矩形后,命中的target变成卡片元素;而onShield挂在 window 捕获 阶段(为了抢在鲸鱼之前),只要shieldTarget(ev.target)命中卡片就stopImmediatePropagation()—— 挂在 document 捕获上的onMove收不到这一笔,paint()不再更新,框就冻住了。两处改动(双保险,缺一不可):
- 遮罩提到
z-index:2147483002,压在卡片(2147483001)和 toast 之上 → 正常情况靶点不会再是卡片。 onShield开头加if (state.active) return—— 框选进行中一律不接管指针事件, 防止卡片被浏览器提升为合成层时命中测试仍然落到卡片上。
验收(2026-10-28 13:52,客户区 1919×1232):
- 起手压在 todo 文字
(700,800)→ 拖到预览卡内部(1400,1000)→ 落盘shot-20261028-135242-199.png700×200,与拖拽量 1:1。 - 逐帧蓝框 bbox:
n1= x700..1405 / y800..1005(右下角贴着鼠标,卡片区域被框住)。 - 三个自测:
selftest-row.mjs18 项、selftest-drag.mjs、selftest-card.mjs20 项,全 PASS。
- 遮罩提到
v1.7.0 — 修「截出来的图尺寸不对 / 看着框选没生效」的两个真 bug:
起因是用户报「任务这些部分截不了吗」,附图上蓝框停在 todo 面板左上角一动不动。 查下来蓝框其实一直在跟着鼠标走,只是肉眼看不见;真正错的是落盘尺寸。
- Bug 1:
capture.ps1的 DPI 声明写错了,整句静默失效。
真实签名返回# 错(v1.7.0 之前) public static extern IntPtr SetProcessDpiAwarenessContext(IntPtr ctx); # 对 public static extern bool SetProcessDpiAwarenessContext(IntPtr ctx);BOOL,声明成IntPtr后 .NET 找不到匹配入口点,异常被try/catch吃掉;退路SetProcessDPIAware()在进程已设过 DPI 上下文时也无效。 于是powershell.exe以「未感知虚拟化」运行,GetClientRect返回 1279×821(= 物理 1919×1232 ÷ 1.5),CopyFromScreen也只抓了左上 1/1.5 的区域 再被渲染成整幅 —— 落盘图只有 67% 分辨率,还整体偏移。 - BOM 是隐藏开关(踩过的坑,记下来)。 同一个文件、字节只差 3 个字节:
无 BOM → 稳定 1279×821;有 BOM → 稳定 1919×1232。原因是
powershell.exe -File读无 BOM 的 UTF-8 时按 ANSI/GBK 解码,文件里的中文注释被切坏,Add-Type的 C# 源码跟着变了样 → 编译产物与调用行为不一致。lib/capture.ps1必须带 UTF-8 BOM,sync-to-profile.sh不要剥掉它。 - 顺带修了「看不见框」:蓝框原来没有
z-index,被z-index:2147483000的遮罩 压在下面,30% 压暗 + 1.5px 边框 → 视觉上几乎不可见。现在z-index:2147483600、2px solid #6fb2ff、外发光,罩内压暗由18%提到34%。 sync-to-profile.sh补上lib/capture.ps1—— 之前漏了它,改了不同步。
验收(2026-10-28 13:41,物理像素,客户区 1919×1232 origin
(320,185)):- 注入真实鼠标长按
(900,830)→ 拖到(1450,990)→ 落盘shot-20261028-134147-114.png550×160,与物理拖拽量 1:1 完全一致。 capture.ps1连跑 4 次全部client 1919x1232 -> out 960x616(0.25/0.25/0.5/0.5 裁剪)。- 逐帧像素检测蓝框 bbox:
g1900,830→1004,874;g2→1204,934;g3→1454,994, 右下角始终贴着鼠标。
- Bug 1:
v1.4.0 — 预览卡能拖了,也不再穿透到桌宠:
修用户报的两件事:「这个框不能拖动,而且默认位置点关闭会穿透点到里面的桌宠」。
- 可拖动:只有
header是把手(._lps-card header),pointerdown落在[data-act](按钮/×)上直接 return。拖动用setPointerCapture+ 位移阈值 3px(dx*dx+dy*dy >= 9),位置夹在视口内留 8px 边距,松手writePos()写localStorage。位置用left/top内联写(applyPos里把right/bottom清成auto)。 - 不穿透:
SHIELD的 7 个事件(pointerdown/move/up/cancel、click、mousedown/up)挂在 window 捕获,stopImmediatePropagation就地截断。 鲸鱼挂件的onDocClickStopper挂 document 捕获 +isWhaleHit()几何命中 (不看e.target),卡片压上去它照样吞 click —— window 捕获先跑才能挡住。 - 默认位置避开鲸鱼:读
.dshwv-root的getBoundingClientRect(),卡片矩形 与它相交就往上/往左推。 - 拖动松手补发的
click用dragSuppressOnce吃掉(pointerdown时清零), 避免终点压在×上误关卡片。
新增
selftest-card.mjs(20 项全 PASS)+mouse-sim.ps1(真机鼠标模拟)。真机验收(2026-10-28 12:38–12:42,物理像素,DSH 窗口原点
(309,185)1941×1243):- 长按拖框
(1000,400)→(1400,700)→ 落盘shot-20261028-123812-421.png267×200 (= 400/1.5 × 300/1.5,150% 缩放对得上),预览卡出现在右侧且不与鲸鱼相交。 - header 拖动
(1959,305)→(1559,355)→ 卡片左移约 395、下移约 50 物理像素,与拖拽量一致。 - 点
×关闭 → 卡片消失,鲸鱼无反应。 - 最极端:把卡片拖到鲸鱼身上(气泡被压住),再点
关闭→ 卡片正常关闭, 鲸鱼既不弹话也不做动作。
教训:
edit/write是「写临时文件再改名」,inode 会换 → 与 profile 的硬链接断掉, 改完必须bash sync-to-profile.sh。另外真机截图坐标是相对窗口图像, 传给mouse-sim.ps1前要加上窗口原点(309,185),否则点偏。- 可拖动:只有
v1.2.0 — 修「重启后长按彻底没反应」:
新增的 loader 注入行
DESKTOP_ROW_TEXT里,probe()结尾写成...then(pick).then(apply)}+}).catch(...),}})多一个}。 浏览器在解析期就整段丢弃这个<script>,连它自己的try{}catch都进不去, 所以shot.js一次都没被加载。表现极具误导性:三条路由全 200、/version返回{"ok":true,"version":"1.1.0"}、shot.js字节数也对,只有页面上什么都不发生。修法:删掉那一个
},new Function(DESKTOP_ROW_TEXT)从SyntaxError: missing ) after argument list变成 OK。 新增selftest-row.mjs(18 项)把这类错误钉死在自测里 —— 把旧代码放回去它会报 10 项 FAIL。教训:注入行是「字符串拼出来的 IIFE」,拼错一个括号是静默全灭,不是局部失效。 所以
--check lib/index.js通过不代表注入行可用(index.js语法没问题, 坏的是它拼出来的那个字符串),必须单独跑selftest-row.mjs。v1.1.0 — 两件事:
- 修框选:
beginSelection以armed=true继承"按住这一笔",遮罩不再等新的pointerdown(按键已经按下,系统不会再发一次,原来dragging永远 false → 每次落整窗)。 双击改自己判(ev.detail会被触发那一次点击污染);加Enter抓整窗。 - 版本自愈:新增
GET /dsh-longpress-shot/version,注入行里带 loader 每 5s 轮询, 版本不一致就location.reload()—— 解决打包版没有Ctrl+R的问题。
验收记录(2026-10-26 20:45):模拟
(600,400) → (900,650)长按拖拽, 落盘~/.dsh/longpress-shots/shot-20261026-204542-085.png尺寸 300×250,与选区一致。- 修框选:
v1.0.0 — 初版,框选实际不可用(
dragging永远 false → 每次落整窗)。
No comments yet. Be the first to write one.