dsh-screenshot-xn(DSH 截图)
简体中文 | English
给 DSH Desktop 加一个截图插件:点输入框旁的按钮(或按 Alt+A)→ 屏幕整体变暗、出现独立的全屏截图面板(微信式:覆盖整屏含任务栏,DSH 窗口先被临时隐藏,所以你看到的就是真实桌面)→ 框选并标注(矩形/椭圆/箭头/画笔/马赛克/文字,可撤销重做)→ 点「插入对话/复制/另存为」,面板关闭,结果落到 DSH 里。
同一块选区还能识别文字和翻译:工具栏最左边的「识别文字」把框选区域读成可复制、可编辑的文本(默认引擎 PP-OCRv6,在本机跑,离线、无密钥;模型没就绪时回落 Windows 自带 OCR),「翻译」再把它翻成 9 种语言的任意一种(用你在 DSH 里已经配好的模型,默认零配置);原文与译文都落在同一张结果卡片上,各自带「复制 / Copy」。
效果预览
下面三张都是真机实拍(本机 3440×1440 主屏),没有摆拍、没有合成:
① 框选 + 标注 + 区域识别(面板) —— 拖出选区后工具栏出现(第一行最左边是「识别文字」与「翻译」两个按钮),红框/椭圆、黄色箭头与手绘曲线、两个文字标注都画在选区里;下方是「识别文字」的结果卡片(引擎 ppocr-v6-small、耗时、行数,行尾「复制 / Copy」)。识别读的是屏幕上原图:画在文字上的红框不会进识别结果。
全屏截图面板:框选 911×303,画了矩形/椭圆/箭头/手绘曲线与两个文字标注,下方是识别文字结果卡片(ppocr-v6-small,3 行,205 ms)
② 抓屏模式(右键截图按钮) —— 「穿透截图(隐藏 DSH)」与「普通截图(含 DSH 窗口)」,当前选中的那条前面是蓝点;选择会被记住,左键 / Alt+A 都用它。
③ DSH 插件页里的同一项配置 —— 插件 → DSH 截图(xn),下拉框写的是同一份 captureMode,改完立即生效、重启保留。
插件名:dsh-screenshot-xn(2026-09-30 由 dsh-screenshot 改名,用于和市面上同名/近名的截屏插件区分)。
目录(改名当天)仍是 E:\dshplugins\dsh-screenshot;改的只是包名与 cordis 行 id/name(插件列表与插件页显示的就是它)。
2026-10-11:仓库目录迁到 E:\dsh-screenshot-xn\dsh-screenshot(与仓库名对齐,老路径 E:\dshplugins\dsh-screenshot 作废)。目录一搬,profile 里那条 link: 就指向了不存在的路径 —— 在 DSH Desktop 2.0.17 上这是启动级故障(整个应用起不来),不是「插件不生效」:见「故障排查」第一行。
保持不变(都是冻结契约,改名不碰):路由 /api/dsh-screenshot/*、度量头 X-DSH-Screenshot、
槽位条目 id dsh-screenshot.button / dsh-screenshot.overlay、DOM 标记 data-dsh-screenshot-ui。
同名迁移已做:活动 profile 的注册(依赖键 / bundles / 行 id+name)、captureMode 持久化覆盖、node_modules 链接。
- 代码产地:
E:\dsh-screenshot-xn\dsh-screenshot(2026-10-11 起;此前为 E:\dshplugins\dsh-screenshot)
- 运行时:DSH Desktop(Windows)。宿主半依赖 Windows PowerShell 5.1 + .NET
System.Drawing(系统自带,无第三方原生依赖);独立面板用系统浏览器(Edge / Chrome)的 kiosk 窗口承载;客户端半是纯浏览器代码。
- 验证过的版本:DSH 0.1.7-rc.2、活动 profile
comfyui、node v24.14.1、Windows PowerShell 5.1.26100.9444(R-06:DSH 升级后请重新目视核对槽位与路由)。
改完代码要重启 DSH Desktop:宿主半(index.js 的路由、kiosk 拉起、退出清理)只在 DSH 进程启动时加载。若宿主还是旧版,点截图会看到「独立截图面板不可用…(overlay.start http 404)」并自动回退到下面「局限」里说的 DSH 内覆盖层流程——功能不缺失,但那不是本版本的主路径。
安装
安装由官方 plugin_manager 工具负责,不要手写 profile 的 package.json / cordis.patch.yml,也不要在 profile 目录执行 pnpm。
-
用 plugin_manager 的 install_bundle 指向本目录的绝对路径:
plugin_manager
action: install_bundle
target: E:\dsh-screenshot-xn\dsh-screenshot
该动作会调用 pnpm 把本地目录装进活动 profile,并按 package.json 的 dsh.bundle.patch 应用 cordis.patch.yml。
target 就是本仓库当前所在的绝对路径(上面是这台开发机上的路径;换机器 / 搬目录请换成你自己的)。装完后 profile 的 package.json 里会留下一条 "dsh-screenshot-xn": "link:<该路径>",<profile>/node_modules/dsh-screenshot-xn 也指向同一目录。
这两处任意一处指向不存在的路径,DSH Desktop 2.0.17 就拒绝启动(症状与修法见「故障排查」第一行)。
-
需要批准:安装动作(以及可能的构建脚本)会请求用户批准;未批准时安装不会生效。若返回体里出现 pendingBuilds,本插件不需要任何构建脚本,不要为它授权。
-
记录 install_bundle 返回的 application 字段(DoD A-7):
application | 含义与后续动作 |
|---|
applied | 当场生效(客户端半热加载);宿主半的路由仍以进程启动时加载的那份为准,改过 index.js 请重启 DSH |
restart-required | 需要重启 DSH Desktop 后再核对 |
overridden | 有更高优先级的层覆盖了本行,检查 profile 的 patch 层 |
failed | 看返回体的 warnings/错误原因;常见原因是 target 不是本目录的绝对路径 |
前置开关(每一台新机器都要看):整屏面板需要 DSH Desktop 放行"普通浏览器访问"。
主路径的「独立整屏截图面板」是由系统浏览器(Edge / Chrome 的 kiosk 窗口)去加载
http://127.0.0.1:<DSH 端口>/api/dsh-screenshot/overlay/page 实现的;而 DSH Desktop 的 webServer
默认只放行带渲染器令牌的请求 —— 实测 DSH Desktop 2.0.15 里 lib/webserver.js 的 permits()
每次请求现查 desktopBrowserAccess,lib/desktop-browser-access-*.js 的 decideDesktopBrowserAccess
在 ordinaryBrowserEnabled 为 false(=openBrowser 的默认值)时对一切非 Electron 渲染器请求返回
denied,于是响应是 403 + 正文就 9 个字节 forbidden。
在默认设置下,kiosk 窗口拿到的就是这个 403:插件在抓屏之前就会预检出来(desktop-browser-access-denied),
给出可见提示,并自动回退到「DSH 内覆盖层」流程(DSH 窗口里先出一张冻结帧,再在这张图上框选)。
这不是故障,也不是装的版本不对 —— 功能不缺失,但那不是主路径。
打开方式(任选其一,改完重启 DSH Desktop;host 半只在进程启动时加载):
配置
配置写在 cordis.patch.yml 那一行的 config: 里(config: {} 即全部使用默认值)。所有键都是可选的,未知键会被忽略。
其中 captureMode 比较特别:它由 Config schema 声明为 volatile 字段,并由 client 半边贡献到 DSH 插件页的 plugins.bundle.config(key = 包名 dsh-screenshot)—— 所以它会以「截图模式 / Capture mode」下拉框出现在 插件 → dsh-screenshot 那一页,和语音输入插件的「识别服务 / 识别语言」同一套机制。你在那里(或右键菜单里)改的值由 DSH 写进当前 profile 的 cordis.patch.yml(不是本插件的 bundle patch),改完立即生效、重启保留。手写 config: { captureMode: normal } 同样有效。
注:DSH 插件页只在有客户端贡献时才渲染配置区(renderSlot("plugins.bundle.config", …, { entryKey: pkg.name }) 外面那层 configured ? … : null),所以这一项必须由本插件的 client.js 贡献 —— 这是 t74 补上的最后一环。
长这样(真机实拍):见「效果预览」的 ③ 插件页切换模式。
- insert:
- id: dsh-screenshot-xn
name: dsh-screenshot-xn
config:
timeoutMs: 20000
cacheTtlMs: 1500
keepTempFile: false
抓屏(宿主)
| 键 | 默认值 | 含义 |
|---|
captureMode | through | 可持久化的抓屏模式(through / normal),也是设置页 / 插件管理里唯一渲染出来的那一项:声明为 Config 的 volatile 字段,改完立即生效并写进当前 profile 的 patch,重启后保留。右键菜单选的模式写的就是它。 |
enabled | true | false 时插件不注册任何路由(等同临时停用) |
timeoutMs | 20000 | 单次抓屏的上限;超时后子进程被终止并返回 502 capture.failed(capture.timeout) |
cacheTtlMs | 1500 | 抓屏结果的短 TTL 缓存:并发 fetch 由"单飞"合并成一次抓屏;缓存引用在 TTL 后释放(D-9) |
scriptPath | lib/capture.ps1 | 抓屏脚本路径;相对路径按包目录解析,也可给绝对路径(D-6 的故障注入点) |
outDir | %TEMP%\dsh-screenshot-xn | 抓屏 PNG 的临时目录(读完即删,见 keepTempFile) |
tag | dsh-screenshot-xn | 临时 PNG 的文件名前缀 |
keepTempFile | false | true 时保留临时 PNG,便于排查(注意会累积磁盘占用)。t75 起它同样是识别用的区域 PNG 与剪贴板用的文本文件的开关 |
powershellPath | %SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe | PowerShell 引擎路径;不存在时回退到 PATH 里的 powershell.exe |
hideWaitMs | 250 | 隐藏 DSH 窗口后等待多久再抓屏。取值被夹到 150–400 ms |
hideMethod | hide | hide(SW_HIDE,窗口完全消失)或 minimize(SW_MINIMIZE,走最小化动画) |
dshImage | 由 process.execPath 推导(如 DSH Desktop) | 用来确认"这是我自己的窗口"的映像名。只有 pid/映像名/标题之一能对上,才允许隐藏 |
dshTitleHint | DSH | 标题包含该片段的窗口才算 DSH 的(最后的兜底判据) |
dshPid | 插件宿主进程的父进程 pid |
独立截图面板(覆盖层,B1)
| 键 | 默认值 | 含义 |
|---|
overlayBrowserPaths | Edge ×2 → Chrome ×2(C:\Program Files…) | 可用的浏览器可执行文件候选,按顺序取第一个存在的;给了自定义列表就不再去 PATH 里找 |
overlayDir | 包内 overlay/ | 面板页面目录(index.html + overlay.js + overlay.css),由宿主同源提供 |
overlayHostScript | lib/overlay-host.ps1 | 浏览器查找 / kiosk 探活 / 关窗与强杀的辅助脚本 |
overlayHeartbeatMs | 5000 | 心跳静默上限:面板页面每 1 s 发一次心跳,超过这个窗口没消息就判会话 aborted(夹到 1000–5000) |
overlayTimeoutMs | 120000 | 单次面板会话的上限;到点判 timeout 并关掉 kiosk 窗口(夹到 5 s–15 min) |
overlayRetainMs | 30000 | 会话结束后帧/结果还保留多久(客户端要在 ready 后取走结果 PNG) |
overlayBodyLimitBytes | 33554432 | 面板回传结果的上限(32 MB) |
overlayLaunch | 无(用真实浏览器) | 仅供离线测试:用自定义 argv 替身替换浏览器拉起命令,${url} / ${token} 会被替换;也可用环境变量 DSH_SCREENSHOT_OVERLAY_LAUNCH(JSON 数组或空格分隔命令) |
区域识别与翻译(t75)
| 键 | 默认值 | 含义 |
|---|
ocrEnabled | true | false 时识别接口回 503 ocr.disabled(面板上仍看得见按钮,点了会说明原因) |
ocrScriptPath | lib/ocr.ps1 | 识别脚本路径;相对路径按包目录解析。它跑在 Windows PowerShell 5.1 上(powershellPath,也是 capture.ps1 用的那个引擎)—— 只有 5.1 还投影 Windows.Media.Ocr 的 WinRT 类型 |
ocrLanguage | 空(自动) | BCP-47 标签,例如 zh-Hans-CN / en-US;空 = 用 Windows 用户配置的语言顺序。脏值(含引号、分号、超长变体)一律回落"自动",不会带进脚本参数 |
ocrTimeoutMs | 20000 | 单次识别的上限;超时回 504 ocr.timeout |
识别引擎(t77:默认换成 PP-OCRv6,本地跑)
Windows 自带引擎对小字中文截图很差(同一张 1978×1059 的实机截图,「截取屏幕、框选并标注,然后复制、另存为或插入当前对话。」
被读成「截取驛幂梃选并标注 然 制 、 另 存 为 或 入 当 前 对 话」)。所以默认引擎换成了
PP-OCRv6(检测 + 识别)跑在 onnxruntime-node 上:同样这张图,它能读成
DSH 截图 v1.0.0 / dsh-screenshot / 截取屏幕、框选并标注,然后复制、另存为或插入当前对话。 /
穿透截图(隐藏 DSH) / Through (hide DSH),耗时约 1.6s(CPU、960 长边、30 行)。
| 键 | 默认值 | 含义 |
|---|
ocrEngine | auto | auto = 模型就绪就用 PP-OCR,否则回落 Windows 引擎;onnx = 强制 PP-OCR(不可用即 503 ocr.unavailable,不静默降级);windows = 只用自带引擎 |
ocrModelTier | small | tiny / small / medium(检测 + 识别两个模型)。体积:tiny ≈ ?/ small ≈ 9.9MB + 21.2MB / medium 更大,质量递增 |
ocrModelDir | ~/.dsh/dsh-screenshot-ocr | 模型存放目录(用户数据目录,不是临时目录);删掉即回到 Windows 引擎 |
ocrModelSource | ModelScope 官方直链 | 模型下载源前缀;需要自建镜像/代理时改它(默认源国内可直连) |
ocrDownloadModels | true | 首次识别时按需下载模型(SHA256 校验,写入 .part 再原子改名)。false = 永不联网,只用已有模型或 Windows 引擎 |
ocrDetLimit | 960 | 检测输入长边上限(夹在 320–4096);调大更准更慢 |
依赖与体积:运行时是 npm 包 onnxruntime-node(预编译,免 Python/GPU/管理员),声明为
可选依赖:不允许构建脚本 / 装不上 / 离线时插件照样能装能用,OCR 自动回落 Windows 引擎(auto 档)。
它的 postinstall 会下载原生库,所以安装时允许构建脚本能让 OCR 更强(管理器会问)。这个包把各平台二进制都打进 tarball(实测 287MB),
本插件带一个裁剪助手,只留当前平台:
node lib/prune-onnx-runtime.mjs # 先看能省多少(只报告)
node lib/prune-onnx-runtime.mjs --apply # 真删:288MB → 65MB(win-x64),重装依赖即可恢复
模型与 ONNX Runtime 都不随包分发,也不进 git。许可:PP-OCR 模型与 RapidOCR 清单为 Apache-2.0,
onnxruntime-node 为 MIT;本插件只下载官方直链并按官方 SHA256 校验(清单见 lib/ocr-models.mjs)。
| translateEnabled | true | false 时翻译接口回 503 translate.disabled。翻译会真的调用你的模型(花 token),所以给了独立的开关 |
| translateProvider / translateModel | 空 / 空 | 翻译用哪条模型路由;两个都留空 = 用 DSH 当前默认模型(agentDefaultModel.currentSelection()),所以默认零配置 |
| translateTarget | zh-Hans | 默认目标语言,取值是 lib/ocr.mjs 里那张闭集的 id(zh-Hans/zh-Hant/en/ja/ko/fr/de/es/ru)。它只决定面板下拉框的初值:用户在卡片里改只影响本次会话 |
| translateTimeoutMs | 45000 | 单次翻译的上限;到点用 AbortController 收回请求并回 504 translate.timeout |
| translateMaxTokens | 4000 | 译文输出上限(一屏文字用不到更多) |
| translatePrompt | 空 | 追加到 system 提示词后面的额外要求(例如"术语保持英文"),会与内置规则逐条并列 |
宿主路由(客户端半依赖它,也可用于自检)
| 项 | 值 |
|---|
| 抓屏 | GET /api/dsh-screenshot/capture(其他方法 405 {"ok":false,"error":"method.not_allowed"}) |
| 抓屏成功 | 200,body 是 PNG,Content-Type: image/png、Cache-Control: no-store, max-age=0 |
| 抓屏度量头 | X-DSH-Screenshot: <base64url(JSON)>,形如 {"widthPx":3440,"heightPx":1440,"url":"/api/dsh-screenshot/capture","scale":1,"bounds":{"x":0,"y":0,"width":3440,"height":1440},"viewportCss":{...},"bytes":2202907,"elapsedMs":148,"mode":"through","hiddenMs":325,"restoreOk":true} |
| 抓屏失败 | 502,{"ok":false,"error":"capture.failed","message":"<短文本,例如 capture.timeout: no result within 20000 ms>"} |
| 抓屏查询参数 | vw/vh=抓屏时客户端 CSS 视口宽高;fresh=1 跳过缓存重新抓屏;mode=through 启用穿透抓屏(缺省/其它值=普通模式) |
| 面板会话 | POST /api/dsh-screenshot/overlay/start → {ok:true,token,startMs,mode,hiddenMs,frameBytes},无可用浏览器时 {ok:false,reason:"no-browser"}(不隐藏 DSH、不抓屏) |
| 面板页面与静态资源 | GET …/overlay/page?token=、…/overlay/asset/<name>、…/overlay/lib/<name>.mjs(纯逻辑模块直接给页面 import,不复制一份) |
| 面板帧 / 结果 | GET …/overlay/frame?token=(会话缓存里的冻结帧,刷新页面不会二次抓屏)、GET …/overlay/result.png?token=(ready 后才可用) |
| 面板状态 / 心跳 / 提交 | GET …/overlay/status?token= → `{state:"running" |
| 区域识别 | POST …/overlay/ocr?token=,body {png:"data:image/png;base64,…", language?:"zh-Hans-CN"} → 200 {ok:true,text,lines:[{text,words:[{text,x,y,width,height}]}],language,engine,elapsedMs,empty};失败 400 ocr.bad-body / 503 ocr.unavailable,ocr.disabled / 502 ocr.failed / 504 ocr.timeout |
| 翻译 | POST …/overlay/translate?token=,body {text:"…", target?:"ja"} → 200 {ok:true,text,source,target,provider,model,unchanged,truncated,elapsedMs};失败 400 translate.bad-body / 503 translate.unavailable,translate.disabled / 502 translate.failed / |
注意:度量头用的是 base64url 字母表(-、_,无 = 填充),客户端解码时要先还原成标准 base64。
抓屏链路:index.js → lib/capture.ps1(DPI-aware,SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2))→ 输出 ---JSON-BEGIN--- {...} ---JSON-END---;脚本失败时以非零退出码 + ok:false 的 JSON 报错。优先使用宿主已提供的 subprocess 进程 Service,不可用时回退 node:child_process;两条路径都受 timeoutMs 约束、隐藏子进程控制台窗口、超时强制终止(D-7:无残留进程)。
抓屏时隐藏 DSH(贯穿两条路径)
-
语义:抓屏前把 DSH 自己的窗口临时隐藏(SW_HIDE,或 hideMethod: minimize),等待 hideWaitMs 让桌面合成器重绘,抓一帧,然后立即恢复(SW_RESTORE + SetForegroundWindow)。因此被 DSH 遮住的桌面内容可以截到,而且面板里看到的画面不含 DSH 界面。
-
只动自己的窗口:脚本按 ①父进程 pid ②映像名 ③标题片段 三条判据确认"这是 DSH 的窗口",且必须是可见的顶层窗口、且不是脚本自己。任何一条都对不上就不隐藏,直接回退普通抓屏(响应仍是 200,只是 mode 回到 normal,图里含 DSH)——宁可截到 DSH,也不会去最小化用户的其它窗口。
-
恢复不依赖运气:隐藏+等待+抓屏+恢复全程在脚本的 try/finally 里;恢复判据是"窗口重新可见且未最小化",SW_RESTORE 会重试并依次降级到 SW_SHOW/SW_SHOWNORMAL;如果脚本报 restoreOk:false,宿主还会再跑一次 -RestoreOnly 救援入口(幂等,5 s 上限)。恢复未确认时整次请求返回 502,不会把"可能停在被隐藏状态"的图交给客户端。
-
耗时(本机 3440×1440 实测,脚本内计时取 3 次中位数):
| 路径 | 脚本总耗时 | 其中隐藏段 hiddenMs | 端到端(含进程启动) |
|---|
| 普通(不隐藏) | 89 ms | 0 | ≈ 490 ms |
隐藏再抓(hideWaitMs: 250) | 539 ms | 325 ms | ≈ 980 ms |
隐藏再抓(hideWaitMs: 150) | 442 ms | 230 ms | ≈ 890 ms |
即隐藏比不隐藏多约 350–450 ms。要把面板弹得更快,把 hideWaitMs 调到 150(少等待可能让个别窗口停留在半透明淡出帧上,可自行权衡)。
卸载
两种方式,任选其一:
- 彻底移除:
plugin_manager → action: remove_bundle,target: dsh-screenshot-xn。该动作会从活动 profile 移除包与 bundle 行;按提示重启后按钮消失,DSH 正常启动(F-4)。工作区源码目录不受影响。
- 临时停用:
plugin_manager → action: set_bundle,target: dsh-screenshot-xn,enabled: false。重启后插件不激活,输入框不受影响(D-5);enabled: true 即可恢复。
残留检查:卸载后 %TEMP%\dsh-screenshot-xn 里最多有本插件留下的临时 PNG(正常路径下读完即删);%TEMP%\dsh-screenshot-xn-overlay-<token> 是面板浏览器的一次性 profile,会话结束即删(强杀等异常路径可能留下空目录,可直接删除)。
故障排查
| 现象 | 原因与处理 |
|---|
DSH Desktop 升级到 2.0.17 后整个应用起不来(窗口一闪就没、或只有一个空白页/错误框),日志里是 dsh-plugin-desktop: cannot resolve package "dsh-screenshot-xn" from the Desktop installation or active Profile(错误名 PackageOverlayNotFoundError) | profile 里本插件那条 link: 指向了不存在的路径(典型场景:仓库目录被搬走或改名,见开头的「2026-10-11 迁目录」)。2.0.17 在启动时会拿 profile 的每一行 bundle 走 resolveOverlayPackage(lib/profile-*.js 的 loadRecoveryFilteredProfile → lib/package-overlay-*.js),解析不到就抛错并拒绝启动;2.0.15 及更早只是把这一行当「缺失」跳过,所以这是升级到 2.0.17 之后才显形的。处理(任选其一):① 官方路径:plugin_manager → action: install_bundle、target: E:\dsh-screenshot-xn\dsh-screenshot,它会把这条链接重写到当前路径;② 手工路径:改 profile 的 package.json,让 "dsh-screenshot-xn": "link:<本仓库绝对路径>" 指向真实目录,并让 <profile>/node_modules/dsh-screenshot-xn 指向同一目录(junction 或 symlink 都行,必须是目录链接而不是拷贝)。起不来时的应急通道:把 dsh-screenshot-xn 从 profile 的 dsh.profile.bundles 里去掉(=桌面端把插件「停用」),DSH 立刻能起,之后再按上面修好加回来。自检(不必重启):node -e "const{createRequire}=require('module');console.log(createRequire('<profile>/package.json').resolve('dsh-screenshot-xn/package.json'))" 能打印出路径即已修好;2.0.17 只认落在 <profile>/node_modules 里的那份解析结果(lib/package-overlay-*.js 里那条偏移检查),所以别把 link: 指到 profile 之外的祖先 node_modules、指望它兜底 |
| 输入框旁没有截图按钮 | ① 插件未激活:确认 install_bundle 返回 application: applied,否则重启一次;② 客户端 bundle 未加载:打开开发者工具看是否有 bundle ... loaded without registering "dsh-screenshot";③ 槽位:本插件当前只注册 conversation.input.right(若该槽在 composer 变体下不渲染,保底方案是改注册 conversation.composer.dock,按钮会移到输入卡片下方,见"局限") |
| 点截图后全屏被一层白/灰底挡住,只看得见一行"拖动鼠标框选"和一个输入框 | 面板页的 CSS/JS 没加载成功(没样式化的骨架)。原因(t64 已修):宿主把 token 校验放在静态资源分支之前,而页面的 <link>、<script> 与静态 import '…/overlay/lib/x.mjs' 都不带 token → 三族全 404、脚本一行不跑、没有心跳、5 s 后会话 aborted。修好后必须重启 DSH Desktop(宿主半只在进程启动时加载);页面自带启动看门狗,再遇到这种情况会在屏幕顶部显示红色横幅(含自身 URL),宿主日志里也会有 overlay static file missing: 行 |
| 点截图后提示「独立截图面板不可用…(overlay.start http 404)」 |
功能与交互(对照 DoD B 段)
- 入口:输入框动作区(
conversation.input.right)的截图按钮,文案"截图 / Screenshot";Alt+A 是同一入口(应用内快捷键,见"局限")。
- 两种抓屏模式(右键菜单,C-6):右键单击截图图标弹出模式菜单(真机实拍见「效果预览」的 ② 右键菜单切换模式),选「穿透截图(隐藏 DSH)」(默认)或「普通截图(含 DSH 窗口)」:
- 穿透:抓屏前临时隐藏 DSH、抓完恢复,画面里没有 DSH(正常截图都用这个);
- 普通:不隐藏,画面里就是有 DSH 窗口(用来截 DSH 自己的界面);这种情况下面板的提示行会写明「普通模式:这次画面里包含 DSH 窗口…」,DSH 侧也会给一条「普通模式:本次画面会包含 DSH 窗口」的提示 —— 不会让你以为是插件坏了;
- 选择会被记住(t73):选完立刻写回宿主 → 宿主写进插件配置(
captureMode,也就是设置页 / 插件管理里那一项,落在当前 profile 的 patch 里),因此 DSH 重启后不用重选;左键 / Alt+A 都用记住的模式;菜单本身不触发截图(截图永远由左键 / Alt+A 显式触发)。菜单支持键盘:↑↓ 移动、Enter/Space 选定、Esc 或点外面关闭,按钮上有 aria-haspopup="menu" / aria-expanded。
- 两种切法等价:右键菜单(顺手切)与 DSH 插件页里本插件的「截图模式」下拉框(
插件 → dsh-screenshot-xn → 截图模式,和语音输入插件的「识别服务 / 识别语言」同一套机制,用的是原生 <select>)写的是同一份配置;插件页改完立即生效、无需重启(该字段声明为 volatile,加载器会就地更新运行中的值),DSH 重启后也仍是上次选的值。
- 改了就一定用得上(t74c):插件页控件选完会立刻写进共享会话状态,并且
startShot() 在抓屏前会再跟宿主对一次 —— 不管模式是从右键菜单、插件页、别的窗口还是手改 patch 改的,下一次截图都用最新值。修之前的表现是"插件页能切,但截图仍按右键菜单那次旧选择来"(插件页是另一棵 React 树,只写了宿主配置,没人更新客户端内存那份)。
- 主路径 = 独立全屏截图面板(B1):点按钮 /
Alt+A → POST /overlay/start?mode=…:宿主按当前模式抓一帧(穿透=先隐藏 DSH 再抓、抓完恢复;普通=直接抓),随即用系统浏览器拉起 kiosk 全屏窗口(覆盖整屏含任务栏)打开面板页面 → 面板里框选、标注 → 点「插入对话/复制/另存为」→ 面板把「动作名 + 只含选区与标注的 PNG」交回宿主、自己关闭 → 动作在 DSH 侧执行(插入走已验证的 paste 桥接、复制写系统剪贴板、另存为弹系统对话框),并给出轻提示、焦点回输入框。
- 面板可用性判定在抓屏之前:机器上没有 Edge/Chrome 时
start 直接回答 no-browser;DSH Desktop 没放行普通浏览器访问时回答 desktop-browser-access-denied(判据是预检面板页拿到 403 且正文恰为 forbidden)。两种都不隐藏窗口、不抓屏,界面给可见提示并回退到 DSH 内覆盖层。
Esc / 右键 / 框选小于 8 px:面板关闭,不产图、不写剪贴板、不动草稿(B-4/B-5),DSH 侧给一条中性提示。
- 取消以外的异常结束(面板被直接关掉 →
aborted、超过 overlayTimeoutMs → timeout、宿主失联 → unreachable)都会给出可见错误提示,可重试,不会永久忙态。
- 慢与卡死可区分:DSH 侧按钮在会话期间是忙态(禁用 + 进度光标 +
aria-busy)。
- (,条目自设 ):面板不可用时(无浏览器 / / 宿主未加载 overlay 路由 / 失败)走它,并在界面写明"已改用 DSH 内截图",抓屏();这条路径就是上一轮的完整实现(抓屏 → DSH 窗口内冻结帧 → 框选 → 标注 → 三种输出),功能不缺失。
局限
- 只支持主屏(单屏约定,E-2):抓屏与面板都按主屏处理,双屏/多屏环境下不会跨屏拼接(跨屏属 P2,本轮不做);
npm run capture:probe 会报告 capture_bounds、virtual_screen、screen_count 与 single_screen;多屏时宿主日志会多一行"capturing the primary screen only"。
- 面板需要两个前提:一个系统浏览器(Edge / Chrome),以及 DSH Desktop 放行普通浏览器访问。kiosk 窗口由浏览器的可执行文件承载(默认候选见配置表);而 DSH Desktop 默认不允许普通浏览器访问它自己的端口(
openBrowser 默认 false),面板页会被闸门拒成 403 forbidden。两个前提缺任一个就回退 DSH 内覆盖层——那条路径的画面在 DSH 窗口内,被 DSH 遮住的桌面内容截不到(B1 主路径没有这个问题,因为抓屏时 DSH 已隐藏)。放行方式见「安装」的前置开关。
ALT+A 是应用内快捷键,只在 DSH 窗口聚焦时生效:宿主侧全局热键(PRD F-04/C-1)本轮不做——插件宿主够不到 Electron 的 globalShortcut,注册系统级热键需要常驻进程/原生模块,用户已确认"不做常驻进程"。因此 ALT+A 走的是页面 keydown,焦点不在 DSH 时按无效(面板打开期间 DSH 在后面,按了也不会重复触发);它与部分输入法/系统快捷键可能冲突(冲突时以 DSH 内输入框的表现为准)。
- >4 MB 自动降级(F-23 / D-8):优先输出无损 PNG;超过 4 MB 时先按逻辑分辨率下采样,仍超则转 WebP 有损。降级为 WebP 时产物标签同步跟随实际编码格式(
lib/output.mjs 的 formatOf);阈值与降级方式可配置(lib/capture-plan.mjs 的 DEFAULT_SIZE_POLICY)。面板侧用的是同一份策略,因此面板交回的字节已经是按策略处理过的结果。
- 缩放比 125% / 150% / 200%(D-3):抓屏脚本以 per-monitor-v2 DPI 感知运行,位图应为物理像素(
capture:probe 的 scale 应为 1)。面板页的 1:1 是"屏幕物理像素 : 画布位图 : 冻结帧像素"三者对齐:画布位图尺寸 = 冻结帧像素,画布的 CSS 尺寸 = 视口,冻结帧层用恒等变换绘制(t64 起;此前它误用了标注层的 CSS→设备比例,视口 ≠ 帧尺寸时会把整屏裁成左上角放大图,见 tests/overlay-page.test.mjs 的 1:1 契约与负样本 8)。kiosk 满屏 + 显示缩放 100% 时视口 CSS 宽 = 帧像素宽(本机实测:3440×1440,AppliedDPI=96),三者严格 1:1;若把 kiosk 改成窗口化、或改显示缩放/浏览器缩放,页面的映射前提就不成立(属已知边界,不在本轮范围内)。
- 面板首次弹出有等待(D-1):抓屏腿(隐藏 → 抓 → 恢复)本机实测 ≈890–980 ms,kiosk 冷启动在 spike 里实测 435–458 ms(
docs/stage2-spike/REPORT.md),所以"点按钮 → 面板可交互"的经验值约 1.2–1.5 s。要把这段压短就调小 hideWaitMs(见上文耗时表)。
- 按钮槽位为源码级取证:
conversation.input.right 的 kind/scope 与渲染位置已核实,但 ownerProps 未逐一核实;当前实现只注册这一个槽(没有自动回退),若目视发现它在该 composer 变体下不渲染,保底方案是改注册 conversation.composer.dock。
- 已知缺陷 R5-02:选区把手的可视位置与命中位置错位(评审第 5 轮登记,本轮未修):选区外框上的 8 个把手方块画出来的位置与其可拖拽的命中区域存在偏移(命中判定按坐标 +
HANDLE_TOLERANCE 容差,不依赖方块元素本身)。调整选区大小时以"命中后光标变成对应方向的缩放箭头"为准;不影响框选、标注与输出。
- P1 未做:全局快捷键(F-04)与设置项持久化(F-05)本轮延后;P2(跨屏拼接、滚动长截图、云端抓屏等)不做。
- 仅 Windows:抓屏走 PowerShell +
System.Drawing;识别走 Windows 自带的 Windows.Media.Ocr(同样 Windows 专属)。其它平台插件不注册路由(只记一条日志)。
开发与验证
cd E:\dsh-screenshot-xn\dsh-screenshot
npm test # 脱机纯逻辑单测(node --test)
node tests/overlay-e2e.mjs # B1 端到端复跑(真实宿主路由 + 真实客户端编排,替身窗口)
node tests/overlay-browser-probe.mjs # 真浏览器探测(headless Edge/Chrome + CDP:页面能否取帧画布、心跳能否续命)
node tests/ocr-browser-probe.mjs # t75 端到端:真浏览器面板 × 真实宿主 × 真实 Windows OCR(识别→翻译→复制)
node tests/negative-asset-order.mjs # 负样本:把静态资源顺序打回原形,验证门禁真的会失败
npm run capture:probe # 手动跑一次抓屏脚本,打印 JSON 结果
node --check index.js && node --check client.js # 两半语法检查
cd E:\dsh-screenshot-xn; node validate.mjs E:\dsh-screenshot-xn\dsh-screenshot # 交付物契约自检(A-1..A-6 / D-11 + X-1/X-2/X-3)
npm pack --dry-run # 发布清单(files 白名单:index/client/lib/overlay/locale/icon/patch/README)
npm test 里与 t75 有关的三份(共 34 条):
tests/ocr.test.mjs:lib/ocr.mjs 的纯逻辑 —— 语言标签白名单、脚本参数、CJK 拼行、结果归一化、提示词定界、译文清洗、截断、错误码映射。12 条,完全脱机。
tests/ocr-routes.test.mjs:三条路由的契约 —— token 门/405/400、真实识别(打 tests/fixtures/ocr-sample.png,跑真的 Windows.Media.Ocr)、临时文件不残留、未安装语言的可读报错、翻译的每一种失败码、以及真的写系统剪贴板并换个进程读回来核对。没装 OCR 语言包的机器会自动跳过真实识别那几条。16 条。
tests/ocr-panel.test.mjs:面板侧的静态契约 —— 旁路请求不许撑开 X-1 的冻结动作词表、复制必须经宿主(页面源码里不许出现浏览器剪贴板 API)、结果按选区失效、目标语言只有一份定义、Esc 的先后顺序,外加负样本。6 条。
三个脚本的分工:
tests/overlay-e2e.mjs:stub 抓屏脚本 + 一个"面板窗口替身"(协议与 overlay/overlay.js 完全一致:取帧 → 心跳 → POST {action, png} → 退出)接到真实宿主路由上,再用从 client.js 切出来的真实客户端编排驱动它。五条场景:insert 全链路(结果 PNG 的 SHA256 必须等于面板提交的字节)、cancel 无副作用、面板静态资源免 token(页面引用的 css/js 与 5 个 lib 模块逐个取,数据路由仍必须 404)、模式透传(穿透时抓屏脚本带 -Through、普通时不带,页面 URL 同步带 mode)、老宿主真实 404 → 可见提示 + 回退。
tests/overlay-browser-probe.mjs:真浏览器(headless Edge/Chrome + DevTools 协议)加载宿主提供的面板页,四个阶段:① 读页面内的 window.__overlayState、画布 2×2 四色像素(帧取到没、方向对不对、有没有裁剪)、工具/动作按钮数量;② 让浏览器挂着跑 8 s,确认 /overlay/status 仍是 running(心跳窗口 5 s,页面没跑起来早就 aborted);③ 造一个选区让工具栏出现,逐个点工具验证图标按钮零可见文字、马赛克→粒度出现、文字→字号出现、移动→两组都收起、工具栏恰好两行且上下错开、第一行右侧有输出尺寸,并 Page.captureScreenshot 留一张视觉档(DSH_KEEP=1 时保留工作目录);④ 普通模式的提示行(mode=normal 必须写明画面含 DSH,mode=through 不该有这句)。没有浏览器的机器会打印 [SKIP] 并以 0 退出。
tests/ocr-browser-probe.mjs:t75 的端到端(真浏览器面板 × 真实宿主路由 × 真实 Windows.Media.Ocr,只有模型是替身)。冻结帧由脚本按视口尺寸合成(白底 + 左上角原样贴上 tests/fixtures/ocr-sample.png)—— 这样帧与视口 1:1,正是真机 kiosk 满屏的情形;若帧宽高比与视口不一致,面板的单一比例映射会把选区压扁(这条踩过,见脚本里的注释)。断言覆盖:识别出完整且无中文空格的文本、卡片落在视口内、翻译用的模型路由、换目标语言只重译不重识别(模型调用恰好 2 次 + 宿主只有一条"识别出文字"的日志)、复制后换个进程读系统剪贴板逐字一致、卡片内按指针不动选区、改选区后卡片自动收起、空白区域报「没识别到文字」、Esc 先关卡片。会话进度同时打到 console 并由 CDP 事件通道收在 Node 侧,所以即使页面被关掉也能定位到卡在哪一步。没有浏览器或没装 OCR 语言包的机器打印 [SKIP] 并以 0 退出。
tests/negative-asset-order.mjs:把「静态资源分支在 token 校验之前」这一条改回缺陷状态,确认 validate.mjs X-1 与 e2e 的静态资源场景都会失败(防"怎么写都绿")。
文案的两处表达:宿主卡片文案取 locale/*.json 的 meta(DSH 官方约定:只读 meta.title/meta.description,en.json 作语言 fallback);UI 文案的键集与 client.js 的 TEXT 一一对应(客户端直接渲染「中 / En」并列标签,满足 DoD B-1 的"截图 / Screenshot"),改文案请同时改 client.js 与两个 locale 文件。
目录结构:
dsh-screenshot/
├─ package.json # type:module + exports + dsh.bundle.patch + dsh.client
├─ cordis.patch.yml # 只 insert 自己一行
├─ index.js # 宿主半:apply(ctx, config) + 抓屏路由 + overlay 路由族(会话状态机 / kiosk 进程管理)+ 识别/翻译/剪贴板三条旁路
├─ client.js # 客户端半:按钮 + B1 编排(start→轮询→取图→在 DSH 内执行动作)+ DSH 内覆盖层(回退)
├─ overlay/ # 独立全屏面板页面(在系统浏览器 kiosk 窗口里运行,纯逻辑 import 宿主的 lib/*.mjs)
├─ lib/ # 可复用纯逻辑(几何/标注/历史/输出/抓屏计划/识别与翻译)+ 四个 ASCII-only 脚本:
│ # capture.ps1(抓屏)· overlay-host.ps1(浏览器/kiosk)· ocr.ps1(Windows.Media.Ocr)· clipboard.ps1(写系统剪贴板)
├─ tests/ # 脱机单测 + 三个复跑脚本(overlay-e2e / overlay-browser-probe / ocr-browser-probe)+ negative-asset-order.mjs(负样本证明)
├─ locale/ # Plugin Manager 卡片文案(meta)+ 与 client.js TEXT 一一对应的双语 UI 文案(ui)
├─ icon.svg # 插件图标(≤256 KiB,无外部引用)
├─ README.md # 中文说明(本文件)
└─ README.en.md # 同一份说明的英文版(English README)