ntesicn/dsh-screenshot-xn ↗★ 0

dsh-screenshot-xn

DSH 截图(xn):整屏微信式框选 + 标注(矩形/椭圆/箭头/画笔/马赛克/文字,可撤销重做),一键插入对话 / 复制 / 另存为;框选区域可 OCR 识别文字并翻译(离线识别 + 复用 DSH 已配模型)。抓屏模式(穿透 / 普通)可在插件页切换并记住,重启不用重选。 适合需要在对话中快速截图、标注、提取图片文本或进行翻译的用户。

パッケージ
dsh-screenshot-xn
互換性
未検証
バージョン
1.0.1
ライセンス
NOASSERTION
最終更新
2026/10/04

インストール

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:ntesicn/dsh-screenshot-xn

ドキュメント

README 全文を読む ↗

配置

配置写在 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

抓屏(宿主)

键默认值含义
captureModethrough可持久化的抓屏模式(through / normal),也是设置页 / 插件管理里唯一渲染出来的那一项:声明为 Config 的 volatile 字段,改完立即生效并写进当前 profile 的 patch,重启后保留。右键菜单选的模式写的就是它。
enabledtruefalse 时插件不注册任何路由(等同临时停用)
timeoutMs20000单次抓屏的上限;超时后子进程被终止并返回 502 capture.failed(capture.timeout)
cacheTtlMs1500抓屏结果的短 TTL 缓存:并发 fetch 由"单飞"合并成一次抓屏;缓存引用在 TTL 后释放(D-9)
scriptPathlib/capture.ps1抓屏脚本路径;相对路径按包目录解析,也可给绝对路径(D-6 的故障注入点)
outDir%TEMP%\dsh-screenshot-xn抓屏 PNG 的临时目录(读完即删,见 keepTempFile)
tagdsh-screenshot-xn临时 PNG 的文件名前缀
keepTempFilefalsetrue 时保留临时 PNG,便于排查(注意会累积磁盘占用)。t75 起它同样是识别用的区域 PNG 与剪贴板用的文本文件的开关
powershellPath%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exePowerShell 引擎路径;不存在时回退到 PATH 里的 powershell.exe
hideWaitMs250隐藏 DSH 窗口后等待多久再抓屏。取值被夹到 150–400 ms
hideMethodhidehide(SW_HIDE,窗口完全消失)或 minimize(SW_MINIMIZE,走最小化动画)
dshImage由 process.execPath 推导(如 DSH Desktop)用来确认"这是我自己的窗口"的映像名。只有 pid/映像名/标题之一能对上,才允许隐藏
dshTitleHintDSH标题包含该片段的窗口才算 DSH 的(最后的兜底判据)
dshPid插件宿主进程的父进程 pid首选的窗口归属进程(Electron 主进程)。脚本还会用映像名/标题交叉确认

独立截图面板(覆盖层,B1)

键默认值含义
overlayBrowserPathsEdge ×2 → Chrome ×2(C:\Program Files…)可用的浏览器可执行文件候选,按顺序取第一个存在的;给了自定义列表就不再去 PATH 里找
overlayDir包内 overlay/面板页面目录(index.html + overlay.js + overlay.css),由宿主同源提供
overlayHostScriptlib/overlay-host.ps1浏览器查找 / kiosk 探活 / 关窗与强杀的辅助脚本
overlayHeartbeatMs5000心跳静默上限:面板页面每 1 s 发一次心跳,超过这个窗口没消息就判会话 aborted(夹到 1000–5000)
overlayTimeoutMs120000单次面板会话的上限;到点判 timeout 并关掉 kiosk 窗口(夹到 5 s–15 min)
overlayRetainMs30000会话结束后帧/结果还保留多久(客户端要在 ready 后取走结果 PNG)
overlayBodyLimitBytes33554432面板回传结果的上限(32 MB)
overlayLaunch无(用真实浏览器)仅供离线测试:用自定义 argv 替身替换浏览器拉起命令,${url} / ${token} 会被替换;也可用环境变量 DSH_SCREENSHOT_OVERLAY_LAUNCH(JSON 数组或空格分隔命令)

区域识别与翻译(t75)

键默认值含义
ocrEnabledtruefalse 时识别接口回 503 ocr.disabled(面板上仍看得见按钮,点了会说明原因)
ocrScriptPathlib/ocr.ps1识别脚本路径;相对路径按包目录解析。它跑在 Windows PowerShell 5.1 上(powershellPath,也是 capture.ps1 用的那个引擎)—— 只有 5.1 还投影 Windows.Media.Ocr 的 WinRT 类型
ocrLanguage空(自动)BCP-47 标签,例如 zh-Hans-CN / en-US;空 = 用 Windows 用户配置的语言顺序。脏值(含引号、分号、超长变体)一律回落"自动",不会带进脚本参数
ocrTimeoutMs20000单次识别的上限;超时回 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 行)。

键默认值含义
ocrEngineautoauto = 模型就绪就用 PP-OCR,否则回落 Windows 引擎;onnx = 强制 PP-OCR(不可用即 503 ocr.unavailable,不静默降级);windows = 只用自带引擎
ocrModelTiersmalltiny / small / medium(检测 + 识别两个模型)。体积:tiny ≈ ?/ small ≈ 9.9MB + 21.2MB / medium 更大,质量递增
ocrModelDir~/.dsh/dsh-screenshot-ocr模型存放目录(用户数据目录,不是临时目录);删掉即回到 Windows 引擎
ocrModelSourceModelScope 官方直链模型下载源前缀;需要自建镜像/代理时改它(默认源国内可直连)
ocrDownloadModelstrue首次识别时按需下载模型(SHA256 校验,写入 .part 再原子改名)。false = 永不联网,只用已有模型或 Windows 引擎
ocrDetLimit960检测输入长边上限(夹在 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: ,形如 {"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":""}
抓屏查询参数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/、…/overlay/lib/.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 / 504 translate.timeout
复制文本POST …/overlay/clipboard?token=,body {text:"…"} → 200 {ok:true,chars,elapsedMs}。页面自己不碰剪贴板:它把文本交给宿主,宿主用 lib/clipboard.ps1 写系统剪贴板(tests/overlay-page.test.mjs 的 t57-4 钉着这条)
未知 token404 {"ok":false,"error":"overlay.unknown-token"}(所有数据路由都先校验 token,含上面三条)

注意:度量头用的是 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 ms0≈ 490 ms
    隐藏再抓(hideWaitMs: 250)539 ms325 ms≈ 980 ms
    隐藏再抓(hideWaitMs: 150)442 ms230 ms≈ 890 ms

    即隐藏比不隐藏多约 350–450 ms。要把面板弹得更快,把 hideWaitMs 调到 150(少等待可能让个别窗口停留在半透明淡出帧上,可自行权衡)。