用 DeepSeek-V4.1-Flash 做「截图即问答」:Windows 抓屏、防截屏浮窗与流式渲染
一、这个工具解决什么问题
做远程协助和坐席辅助的时候,最麻烦的一步永远是「让对方把看到的东西描述清楚」。屏幕上的信息本来就在那儿,用户却要一个字一个字念出来:界面路径、报错文本、表格里的数字。语音识别顶多解决一半,剩下的还是靠人脑转述。
于是有了一个很小的实验品:在浏览器里留一颗按钮,点一下,宿主机把当前页面抓成图,连同少量结构化文本一起发给多模态模型,答案以流式方式显示在一个置顶浮窗里。整个链路里,网页只负责「告诉宿主机现在在哪一页」,其余全部在宿主机进程完成。
三周下来,几个关键决策都踩过坑也验过数,写在这里给同类场景的人省点时间。
二、只抓「窗口客户区」,别抓整屏
第一个直觉方案是截取整个屏幕。它有两个立刻暴露的问题:多显示器下边界算不准;桌面上任何遮挡(聊天窗口、任务栏弹窗)都会进画面,模型会一本正经地分析错内容。
改成只抓目标浏览器窗口的客户区之后,画面就是干净的网页视口。具体做法是两条路各拿一张,再挑「内容更丰富」的那张:
PrintWindow(hwnd, hdc, PW_RENDERFULLCONTENT):渲染的是窗口自身内容,桌面上盖了什么它都不管;- 屏幕
BitBlt到客户区矩形:对 GPU 合成的窗口更靠谱,Chromium 系有时在PrintWindow下返回空白帧。
判断「哪张更好」用的是最朴素的办法——把两张图都缩到 80×45,数不同颜色的个数。网页内容动辄几百到几千色,空白帧只有个位数,差一个数量级,不需要任何模型来判。
def capture_client(hwnd, method="auto"):
screen = capture_screen_rect(*client_rect(hwnd))
win = _crop_client(capture_window(hwnd, "printwindow"), hwnd)
if method == "screen":
return screen
if win is None:
return screen
return screen if _unique_colors(screen) > _unique_colors(win) * 1.15 else win
还有两个容易漏的细节:先设 DPI 感知(per-monitor v2),否则 4K 屏上客户区坐标和实际像素对不上,裁切一定歪;裁切用比例换算而不是 DPR 硬乘,因为浏览器缩放、DPI、窗口边框都会让 CSS 像素与设备像素的比例漂移。让前端把 innerWidth/innerHeight 和题目元素的视口矩形一起上报,宿主端按「截图实际宽度 ÷ 视口宽度」算缩放系数,稳得多。
三、让浮窗「只在屏幕上存在」
这个需求一开始很反直觉:要求浮窗不出现在任何截屏、录屏、屏幕共享里。业务上的理由很正当——浮窗是辅助信息,不该出现在用户录制的教程视频、也不该被客服质检系统误采集成页面内容。
Windows 上有一个用的人不多但非常干净的 API:
user32.SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE) # 0x00000011
它对窗口的要求只有「需要有 HWND」,PyQt / Win32 / WPF 都行。设置之后,窗口照常在屏幕上显示、照常可交互,但 BitBlt、PrintWindow、getDisplayMedia(Chrome / Edge 的屏幕共享)拿到的画面里,它所占的矩形直接变成背后的内容。
验证方式也很简单:把浮窗显示在固定坐标,显示前后对同一块屏幕区域各截一张,做像素差。
before = capture_screen_rect(260, 220, 340, 240)
overlay.show()
after = capture_screen_rect(260, 220, 340, 240)
assert ImageChops.difference(before, after).getbbox() is None # 实测通过
顺带说一句:WDA_EXCLUDEFROMCAPTURE 需要 Windows 10 2004+;老系统上退化成 WDA_MONITOR(采集到黑块),效果能用但不优雅。
四、把图喂给 DeepSeek-V4.1-Flash
模型侧比想象中简单。deepseek-flash(DeepSeek-V4.1-Flash-1017)支持 Vision,走标准 OpenAI 兼容格式,content 从字符串换成数组即可:
messages = [{
"role": "user",
"content": [
{"type": "text", "text": "屏幕内容见附图,按格式作答。"},
{"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{b64}", "detail": "original"}},
],
}]
实测下来的几条约束,值得直接抄进设计里:
| 项 | 实测 / 文档口径 |
|---|---|
| 图片格式 | JPEG / PNG / GIF / WebP,按文件内容判型,不看扩展名 |
| 单图上限 | inline base64 ≤ 32 MiB,请求体 ≤ 48 MiB,单边 ≤ 8192 px |
| 计费 | 每图先缩到约 1300×1300 再编码,单图上限 1024 tokens |
detail |
low 会先缩到 512×512,更快更省;high/original 保留原图 |
| 位置限制 | 图片只能出现在 user 消息里,放进 system / assistant 直接 400 |
| 思考模式 | 默认开启(effort=high),{"thinking":{"type":"disabled"}} 关掉;此模式下 temperature 无效 |
对「截屏问答」这个场景,detail=original + 本地先把长边压到 1600 px 是个不错的折中:再大也会被服务端缩回去,白费带宽;太小则小字识别率明显下降。
还有一点容易忽略:图片块必须替换最后一条 user 消息的字符串内容,而不是新建一条 user 消息。否则多轮里容易堆出两条 user,模型对「哪条才是当前问题」的判断会飘。
五、流式渲染与「思考内容」分离
stream=True 之后是一串 SSE。两个坑:
reasoning_content不是答案。思考模式下每帧可能同时带reasoning_content和content,把两者拼进同一个缓冲区,用户会看到一大段内心独白夹着答案。正确做法是分流:思考内容只用来更新状态栏(「思考中… 1240 字」),答案区只吃content。- 浮窗要边收边滚,完成后再跳回顶部。流式过程中跟随到底,
finish时把光标移回开头——用户要的是「从头读一遍」,不是「停在最后一行」。
choice = (event.get("choices") or [{}])[0].get("delta") or {}
if choice.get("reasoning_content"):
on_reasoning(choice["reasoning_content"]) # 只更新状态
if choice.get("content"):
on_delta(choice["content"]) # 只进答案区
端到端实测:从点击到首字出现约 0.9 s,整段答案(含一次原生检索)2.8 s 完成,输出稳定在 【答案】/【解析】/【置信度】 三段结构里。
六、把「上下文」交给 URL,而不是让人重复描述
同一套界面在不同站点上的语义完全不同。工具里做了一张极简规则表:URL 通配(或正则)命中某条规则,就把对应的场景说明拼进 prompt。
{
"match": "*://*.example.com/console/*",
"context": "这是运维控制台的工单详情页,用户多半在问报错原因;先给结论,再给一步可执行的操作。"
}
没命中时按 URL 词元与页面标题猜一句(exam/console/ticket/订单/报错…),再附上从标题推断出的场景名。实际体验里,这一句上下文能显著减少「模型先复述一遍问题」的废话。
七、安全性上做的几件小事
- 本地桥只绑
127.0.0.1,可选token校验;CORS 只放开需要的头,浏览器端用GM_xmlhttpRequest发送,既不受页面 CSP 限制也不掺入页面自身的鉴权头。 - 凭据只存引用不存明文:配置里写
op://<vault>/<item>/<field>,运行时经本地凭据脚本落成 600 文件的临时副本,进程内再缓存一次。顺带修掉一个隐蔽的性能坑:检索和作答都各自解析 key 时,会每个请求都去敲一次密码库,慢的时候直接把 worker 堵住——加一层进程内缓存后,一个任务的授权调用从 2 次降到 1 次。 - 浮窗默认不抢焦点(
WA_ShowWithoutActivating),避免工具本身打断用户正在输入的内容。
八、可复用的经验清单
- 抓窗口而不是抓屏幕,
PrintWindow与BitBlt各取一张,用颜色丰富度自动择优。 - 需要「不被采集」的 UI,用
SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE),并用像素差做回归验证。 - 图片进模型前先在本地压缩到长边 1600 px 左右,服务端反正会缩到约 1300²、上限 1024 tokens。
reasoning_content与content必须分流,否则答案区会被内心独白淹没。- 上下文从 URL 来,别让人手动描述;规则表比 prompt 里写十句「请理解我的场景」有效。
- 凭据只写引用;进程内缓存解析结果,别让密码库成为请求路径上的同步阻塞点。
代码量并不大:宿主机约 1500 行 Python(抓屏 + 本地桥 + 流式客户端 + 浮窗),浏览器侧不到 400 行 JS。真正花时间的不是模型调用,而是「怎么把屏幕上的东西准确、干净、可复现地交到模型手里」——这一层做扎实了,剩下的事模型自己会做。
- 点赞
- 收藏
- 关注作者
评论(0)