dsh-screenshot-xn(DSH 截图)
简体中文 | English
给 DSH Desktop 加一个截图插件:点输入框旁的按钮(或按 Alt+A)→ 屏幕整体变暗、出现独立的全屏截图面板(微信式:覆盖整屏含任务栏,DSH 窗口先被临时隐藏,所以你看到的就是真实桌面)→ 框选并标注(矩形/椭圆/箭头/画笔/马赛克/文字,可撤销重做)→ 点「插入对话/复制/另存为」,面板关闭,结果落到 DSH 里。
同一块选区还能识别文字和翻译:工具栏最左边的「识别文字」把框选区域读成可复制、可编辑的文本(默认引擎 PP-OCRv6,在本机跑,离线、无密钥;模型没就绪时回落 Windows 自带 OCR),「翻译」再把它翻成 9 种语言的任意一种(用你在 DSH 里已经配好的模型,默认零配置);原文与译文都落在同一张结果卡片上,各自带「复制 / Copy」。
效果预览
下面三张都是真机实拍(本机 3440×1440 主屏),没有摆拍、没有合成:
① 框选 + 标注 + 区域识别(面板) —— 拖出选区后工具栏出现(第一行最左边是「识别文字」与「翻译」两个按钮),红框/椭圆、黄色箭头与手绘曲线、两个文字标注都画在选区里;下方是「识别文字」的结果卡片(引擎 ppocr-v6-small、耗时、行数,行尾「复制 / Copy」)。识别读的是屏幕上原图:画在文字上的红框不会进识别结果。

② 抓屏模式(右键截图按钮) —— 「穿透截图(隐藏 DSH)」与「普通截图(含 DSH 窗口)」,当前选中的那条前面是蓝点;选择会被记住,左键 / Alt+A 都用它。
③ DSH 插件页里的同一项配置 —— 插件 → DSH 截图(xn),下拉框写的是同一份 captureMode,改完立即生效、重启保留。
![]() |
![]() |
| ② 右键菜单切换模式 | ③ 插件页切换模式(同一份配置) |
插件名:
dsh-screenshot-xn(2026-09-30 由dsh-screenshot改名,用于和市面上同名/近名的截屏插件区分)。 目录仍是E:\dshplugins\dsh-screenshot;改的只是包名与 cordis 行 id/name(插件列表与插件页显示的就是它)。 保持不变(都是冻结契约,改名不碰):路由/api/dsh-screenshot/*、度量头X-DSH-Screenshot、 槽位条目 iddsh-screenshot.button/dsh-screenshot.overlay、DOM 标记data-dsh-screenshot-ui。 同名迁移已做:活动 profile 的注册(依赖键 / bundles / 行 id+name)、captureMode持久化覆盖、node_modules 链接。
- 代码产地:
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:\dshplugins\dsh-screenshot该动作会调用 pnpm 把本地目录装进活动 profile,并按
package.json的dsh.bundle.patch应用cordis.patch.yml。需要批准:安装动作(以及可能的构建脚本)会请求用户批准;未批准时安装不会生效。若返回体里出现
pendingBuilds,本插件不需要任何构建脚本,不要为它授权。记录
install_bundle返回的application字段(DoD A-7):application含义与后续动作 applied当场生效(客户端半热加载);宿主半的路由仍以进程启动时加载的那份为准,改过 index.js请重启 DSHrestart-required需要重启 DSH Desktop 后再核对 overridden有更高优先级的层覆盖了本行,检查 profile 的 patch 层 failed看返回体的 warnings/错误原因;常见原因是target不是本目录的绝对路径自检清单(安装 + 重启后 30 秒内可做完):
- 打开任意普通会话,输入框动作区出现截图按钮,悬停显示"截图 / Screenshot";
- 点按钮:DSH 窗口一闪(被临时隐藏后恢复),随后整个屏幕被截图面板盖住(含任务栏);
- 框选一块区域 → 工具栏出现 → 点「复制」→ 面板关闭、回 DSH,出现成功提示;
- 在面板里按 Esc(或右键、或框太小)→ 面板关闭,DSH 里的草稿文字与附件与进入前完全一致;
- 把 Edge / Chrome 都临时改名(模拟"没有浏览器")→ 点截图出现「未找到可用的浏览器…」并照样能截(回退 DSH 内覆盖层)。
配置
配置写在 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 | 首选的窗口归属进程(Electron 主进程)。脚本还会用映像名/标题交叉确认 |
独立截图面板(覆盖层,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 / 504 translate.timeout |
| 复制文本 | POST …/overlay/clipboard?token=,body {text:"…"} → 200 {ok:true,chars,elapsedMs}。页面自己不碰剪贴板:它把文本交给宿主,宿主用 lib/clipboard.ps1 写系统剪贴板(tests/overlay-page.test.mjs 的 t57-4 钉着这条) |
| 未知 token | 404 {"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 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,会话结束即删(强杀等异常路径可能留下空目录,可直接删除)。
故障排查
| 现象 | 原因与处理 |
|---|---|
| 输入框旁没有截图按钮 | ① 插件未激活:确认 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)」 | 宿主半还是旧版(没有 overlay 路由):重启 DSH Desktop。本次截图已经自动回退到 DSH 内覆盖层流程,功能不缺失 |
| 点截图后提示「未找到可用的浏览器(Edge / Chrome)…」 | 机器上没有 Edge/Chrome,或 overlayBrowserPaths 指错了:面板无法全屏承载,本次截图自动回退到 DSH 内覆盖层流程 |
| 点截图后一直转圈 / 提示"抓屏失败" | 宿主抓屏路由没通。先在浏览器 fetch /api/dsh-screenshot/capture 看返回:404 表示路由未注册(插件未激活或 webServer 服务缺席,看 DSH 日志里 [dsh-screenshot] 行);502 表示脚本失败,message 里带 capture.* 细分码(capture.script-missing=scriptPath 指错、capture.timeout=超时、capture.spawn-failed=powershell 找不到) |
点截图后 502 且 message 含 capture.timeout |
单次抓屏超过 timeoutMs。先把 timeoutMs 调大;若仍超时,手动执行 npm run capture:probe 观察耗时(本机实测:脚本自身抓屏 73 ms、含保存 155 ms;Node 拉起进程后端到端约 560 ms) |
| 面板出现了,但 DSH 窗口仍然出现在画面里 | 宿主没能确认"这是 DSH 的窗口"(pid/映像名/标题三条都对不上)→ 脚本按设计回退为"不隐藏"抓屏,并在界面给出可见提示。检查 dshPid / dshImage / dshTitleHint |
| 面板卡住不动,或提交后一直没回 DSH | 面板页面每 1 s 发一次心跳,超过 overlayHeartbeatMs 没消息宿主会判 aborted 并关掉窗口,DSH 侧给出可见提示(可重试);会话总上限是 overlayTimeoutMs |
| 面板提交后 DSH 里没有插入 | 插入走官方 paste 桥接(useInput 的附件数必须增加才算成功);桥接不可用时会真实降级为"复制到剪贴板 + 提示手动 Ctrl+V",不会静默。看控制台 [dsh-screenshot] paste bridge … 行 |
路由 404 |
webServer 未激活(极简 profile)时插件只记一条日志、不注册路由,这是设计如此(插件不会拖垮启动)。换回带 Web 载体的 profile 即可 |
| 坐标对不上(框选区域与粘贴图不一致) | 面板是整屏 1:1:页面按"位图尺寸 = 冻结帧像素、CSS 尺寸 = 视口尺寸"绘制,框选坐标即屏幕坐标。若真的出现偏移,先确认抓屏是 DPI-aware(npm run capture:probe 输出的 dpi_awareness 应含 =True,scale 应为 1),并附面板控制台的 [dsh-screenshot] overlay mapping 日志 |
| 主题切换后 DSH 内面板看不清 | DSH 内覆盖层只用 --dsw-alias-* 令牌;独立面板是独立窗口,自绘深色 UI,不依赖 DSH 主题(这是刻意的:独立窗口拿不到 DSH 的 CSS 变量) |
| 权限/安全软件拦截 PowerShell | 抓屏依赖 powershell.exe 启动。企业策略禁用 -ExecutionPolicy Bypass 或拦截脚本时,把 scriptPath 指到允许的副本,或改用允许的策略重新调用;失败都会以 502 + capture.failed 显式报错,不会静默 |
| 想验证纯逻辑单测 | 在包目录执行 npm test(即 node --test)。用例完全脱机:不读真实屏幕、不联网、不加载 DSH 运行时 |
| 识别说"这台机器没有可用的 OCR 语言包" | Windows 的 OCR 引擎要装语言包:设置 → 时间和语言 → 语言和区域,给中文/英文加上"可选语言功能 → 光学字符识别"。想看这台机器到底装了哪些,直接跑 powershell -NoProfile -ExecutionPolicy Bypass -File lib/ocr.ps1 -ListLanguages;只装了一种也没关系,ocrLanguage 留空即用它 |
| 识别出来是问号/方块 | 那是 PowerShell 的 stdout 编码问题,不是识别问题:重定向的管道默认按 OEM 代码页(中文机器上是 936/GBK)写出,宿主按 UTF-8 读。lib/ocr.ps1 与 lib/clipboard.ps1 顶上已经把 [Console]::OutputEncoding 钉成 UTF-8;自己写脚本调用同一批接口时记得照做 |
| 点翻译没反应/报"没有可用于翻译的模型" | 翻译走 ctx.get('llm') + DSH 的默认模型。先确认这个 profile 里有可用的模型(模型选择器里有值);也可以在配置里显式指定 translateProvider / translateModel |
| 不想让截图功能花 token | 把 translateEnabled 设为 false:翻译接口会回 503 translate.disabled,界面上会说明原因。识别是纯本地的,不受影响 |
功能与交互(对照 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(不隐藏、不抓屏),界面给可见提示并回退。 Esc/ 右键 / 框选小于 8 px:面板关闭,不产图、不写剪贴板、不动草稿(B-4/B-5),DSH 侧给一条中性提示。- 取消以外的异常结束(面板被直接关掉 →
aborted、超过overlayTimeoutMs→timeout、宿主失联 →unreachable)都会给出可见错误提示,可重试,不会永久忙态。 - 慢与卡死可区分:DSH 侧按钮在会话期间是忙态(禁用 + 进度光标 +
aria-busy)。
- 面板可用性判定在抓屏之前:机器上没有 Edge/Chrome 时
- 回退路径 = DSH 内覆盖层(
shell.overlay,条目自设pointer-events):面板不可用时(无浏览器 / 宿主未加载 overlay 路由 /start失败)自动走它,并在界面写明"已改用 DSH 内截图",同样按当前模式抓屏(startCapture(runtime, through));这条路径就是上一轮的完整实现(抓屏 → DSH 窗口内冻结帧 → 框选 → 标注 → 三种输出),功能不缺失。 - 框选:拖拽出矩形,区域内原色、区域外变暗;实时显示"宽 × 高"与十字辅助线;松开后出现选区与工具栏。选区宽或高 < 8 px 视为无效:本次截图取消,不产生图片、不写剪贴板(B-5)。
- 微调:8 个把手改大小(对边固定)、拖动选区内部平移。
- 工具栏(t65/t66/t68):所有按钮都是图标按钮(悬停出名字,
title+aria-label),固定两行(不靠自动折行 —— 折点随机、长短不齐很难看):- 第一行:工具(移动/矩形/椭圆/箭头/画笔/马赛克/文字)+ 撤销/重做,行尾是四个动作图标,顺序为 复制 → ⤓ 另存为 → ✕ 取消 → ✓ 插入对话(✓ 是行尾的确认键,也是默认高亮动作);
- 第二行:颜色(圆形色板,只在会落色的工具下出现)+ 当前工具用得上的档位 + 行尾输出尺寸
宽 × 高 px。
- 颜色与档位按当前工具显隐(t68/t72):颜色只在矩形/椭圆/箭头/画笔/文字下出现 —— 移动工具不落色、马赛克的颜色在
lib里根本不参与绘制(它只做像素化),所以这两者都不显示色板;线宽(细/中/粗)只在矩形/椭圆/箭头/画笔下出现;字号只在文字工具下出现;马赛克粒度只在马赛克工具下出现。每根竖线只在"它后面那个分组可见、且前面已经出现过可见分组"时显示(例如马赛克工具下色板与线宽都收起,粒度前面就不会挂一根孤线)。撤销/重做没有历史时置灰。图标与client.js的toolIcon/undoIcon/redoIcon/closeIcon用同一套 path 数据(动作图标check/copy/download另加),两个入口的工具栏长得一样、显隐规则也一致。 - 标注:矩形、椭圆、箭头、画笔、马赛克、文字六类,颜色/线宽/字号可选(B-6);马赛克块大小随强度设置变化。独立面板与 DSH 内覆盖层用的是同一份纯逻辑(
lib/annotations.mjs等,由宿主以 ES 模块形式给面板 import),不存在两套实现。 - 撤销重做:Ctrl+Z / Ctrl+Y,按操作顺序回退与恢复,原图不动(B-7);对已放置标注的移动/缩放/删除同样各算一条历史(一次拖拽 = 一条历史)。
- 已放置标注的选中 / 移动 / 缩放 / 删除(B-13) —— 面板的流程是**"悬停即手柄、按下即拖"**(t71):
- 悬停即手柄:鼠标进入已绘制标注的范围,光标立刻变成拖拽手柄(
move);在选中标注的角把手上是缩放光标;在选区内部(移动工具下)也是移动光标。不需要先切工具、也不需要先单击选中。 - 按下即拖:在标注上按下就直接拖动它 —— 位移与拖动一致,并被夹在选区内(触边即停);只影响该标注。拖动优先于"画新标注":想在一个已有标注的位置上画新的,先把它拖开(或
Ctrl+Z撤销)。 - 双击改文字:光标处于手柄状态时(即点在已放置的文字标注上)双击 → 输入框出现、回填原文字并全选 → 回车替换(一次编辑 = 一条历史,位置不变);清空文字或按 Esc = 不改动。
- 缩放:拖任一角把手 → 文字改字号、矩形/椭圆/箭头改
rect、画笔/马赛克改bounds;有最小边下限(8 px)。 - 删除:选中态按 Delete 或 Backspace 只删该标注;删除可 Ctrl+Z 撤销恢复。
- 一次拖拽 = 一条历史:中间的每一帧只是预览,松手才写一条历史(Ctrl+Z 一次回到拖动前)。
- 实现口径:复用
lib/annotations.mjs的annotationRect/findAnnotationAt/hitAnnotationHandle/moveAnnotation/resizeAnnotationRect/scaleAnnotation;标注一律按"设备像素"存储(lib 的约定),入口处把指针的视口坐标换算一次,绘制与导出都不再换算 —— 任意缩放比下"看到的、拖动的、导出的"都是同一套坐标(t70 修正:面板此前把标注存成视口坐标、绘制时才乘比例,与 lib 的约定相反,且完全没有拖动逻辑)。 - 与 DSH 内覆盖层的差异:那边的"选中/移动"仍要求先切到「移动」工具(B-13 的原始前提),面板这侧已改为工具无关(t71,用户口径)。要两边完全一致的话说一声,我把那条也改过来。
- 已知缺陷:选区把手的可视位置与命中位置存在偏移,见「局限」R5-02。
- 悬停即手柄:鼠标进入已绘制标注的范围,光标立刻变成拖拽手柄(
- 输出:复制到剪贴板(仅选区内容,含标注、不含遮罩与工具栏)、另存为(默认文件名
DSH截图_yyyyMMdd_HHmmss.png;>4 MB 降级为 WebP 时默认名与扩展名同步变成.webp)、插入对话(默认高亮动作)。 - 插入对话的实现路径(B-10 / B-12):走官方 paste intake 桥接 —— 构造一个
ClipboardEvent('paste')(DataTransfer 里放一张File)派发到输入框编辑器,命中 conversation 自己注册的 paste 命令,与"用户手动 Ctrl+V 粘贴图片"完全同一条代码路径。成功判据是硬的:useInput(s => s.attachmentIds)的长度在窗口内增加才算成功;桥接不可用、事件被拒或没有增量时,真实降级为复制到剪贴板 + 可见提示「已复制截图,可在输入框 Ctrl+V 粘贴」,绝不静默失败。 - 区域识别 + 翻译(t75):工具栏第一行最左边多了两个图标按钮 —— 识别文字(取景框)与翻译(地球),与右边四个"对图片做什么"的动作之间隔一根竖线。框选之后:
- 点识别文字 → 弹出结果卡片,上半张写识别出来的文字,标题行右侧给出「几行 · 引擎语言 · 耗时」,行尾是「复制 / Copy」与关闭 ✕。
- 点翻译 → 卡片两半都填上:上半张原文、下半张译文,标题行右侧是「目标 / Target」下拉框(9 种语言,选项直接来自
lib/ocr.mjs的闭集)、模型路由与耗时,行尾同样是「复制 / Copy」。换目标语言 = 只重译,不重新识别;已经识别过的选区再点翻译不会识别第二遍。 - 卡片贴在工具栏下方(放不下就退到选区上方,最后夹回视口内),可以滚动、文字可选中,在里面按指针不会拖出一个新选区。
Esc第一下只收起卡片,第二下才取消整次截图。 - 识别的是屏幕上的原图:待识别区域从冻结帧原样裁下来,不画标注 —— 你在文字上画了个红框再点识别,红框不会变成识别结果的一部分(马赛克同理:想遮住的东西不该被识别出来)。
- 选区一改,卡片立刻收起:结果按"产生它时的选区"记账,不存在"缩小选区后翻译的还是上一块区域的字"。
- 识别离线:
Windows.Media.Ocr是 Windows 自带的引擎,不要 API Key、不联网。翻译则用你在 DSH 里已经配好的模型(默认零配置),不额外要密钥。 - 选区里没有文字不是错误:卡片会写「没识别到文字 · No text found」。真正的失败各有各的话:没装语言包、识别超时、模型不可用、翻译超时、文本过长(超过 8000 字符按行截断并标注)—— 每一条都有独立的错误码(
lib/ocr.mjs的OCR_ERROR_KEYS把宿主码映射到面板文案,两侧不会各写一套)。
局限
- 只支持主屏(单屏约定,E-2):抓屏与面板都按主屏处理,双屏/多屏环境下不会跨屏拼接(跨屏属 P2,本轮不做);
npm run capture:probe会报告capture_bounds、virtual_screen、screen_count与single_screen;多屏时宿主日志会多一行"capturing the primary screen only"。 - 面板需要一个系统浏览器(Edge / Chrome):kiosk 窗口由它的可执行文件承载(默认候选见配置表)。没有浏览器就回退 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 专属)。其它平台插件不注册路由(只记一条日志)。 - 识别读的是原图,不含标注(t75 的刻意选择):待识别区域直接从冻结帧裁下来,你画的矩形/椭圆/箭头/画笔/文字都不会进识别结果。好处是"在文字上画个框再识别"不会被自己的标注污染;代价是"想连标注一起识别"做不到(那本来也不是识别该做的事)。
- 翻译要花你自己的 token:翻译会用 DSH 当前默认模型发一次文本调用(
translateEnabled可关)。识别完全是本地的、不花钱。 - 单次识别是"一屏文字"的量级:待翻译文本上限 8000 字符(超出按行截断并在卡片上标注"文本过长"),区域边长上限 4096 设备像素(超出等比缩小)。整屏文字识别没问题,但这不是给"整本书"用的。
- 只识别主屏区域(与第 1 条同源):冻结帧只覆盖主屏,所以跨屏选区不存在。
- 识别与翻译只在独立面板里(t75 的这一轮范围):DSH 内覆盖层(第 2 条那条回退路径)没有这两个按钮 —— 它是"机器上没有 Edge/Chrome"时的降级通道,画面还在 DSH 窗口内。真机上的主路径是面板(本机实测 Edge 在装)。要在回退路径上也有,说一声;
lib/ocr.mjs与宿主那三条路由已经是两侧共用的一份,接上去主要是加两个按钮和一张卡片的客户端工作。
开发与验证
cd E:\dshplugins\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:\dshplugins; node validate.mjs E:\dshplugins\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.mjsX-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)


还没有评论,来写第一条。