用 DeepSeek-V4.1-Flash 做「截图即问答」:Windows 抓屏、防截屏浮窗与流式渲染

举报
yd_218013313 发表于 2026/11/02 14:03:25 2026/11/02
【摘要】 从一次「点一下就答」的实验出发:只抓浏览器窗口客户区并按题目区域裁切,用 SetWindowDisplayAffinity 让浮窗不进任何截屏,再把 DeepSeek-V4.1-Flash 的视觉输入与流式输出接进本地工具链。附实测数据、接口约束与踩坑清单。

一、这个工具解决什么问题

做远程协助和坐席辅助的时候,最麻烦的一步永远是「让对方把看到的东西描述清楚」。屏幕上的信息本来就在那儿,用户却要一个字一个字念出来:界面路径、报错文本、表格里的数字。语音识别顶多解决一半,剩下的还是靠人脑转述。

于是有了一个很小的实验品:在浏览器里留一颗按钮,点一下,宿主机把当前页面抓成图,连同少量结构化文本一起发给多模态模型,答案以流式方式显示在一个置顶浮窗里。整个链路里,网页只负责「告诉宿主机现在在哪一页」,其余全部在宿主机进程完成。

三周下来,几个关键决策都踩过坑也验过数,写在这里给同类场景的人省点时间。

二、只抓「窗口客户区」,别抓整屏

第一个直觉方案是截取整个屏幕。它有两个立刻暴露的问题:多显示器下边界算不准;桌面上任何遮挡(聊天窗口、任务栏弹窗)都会进画面,模型会一本正经地分析错内容。

改成只抓目标浏览器窗口的客户区之后,画面就是干净的网页视口。具体做法是两条路各拿一张,再挑「内容更丰富」的那张:

  • 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。两个坑:

  1. reasoning_content 不是答案。思考模式下每帧可能同时带 reasoning_content 和 content,把两者拼进同一个缓冲区,用户会看到一大段内心独白夹着答案。正确做法是分流:思考内容只用来更新状态栏(「思考中… 1240 字」),答案区只吃 content。
  2. 浮窗要边收边滚,完成后再跳回顶部。流式过程中跟随到底,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),避免工具本身打断用户正在输入的内容。

八、可复用的经验清单

  1. 抓窗口而不是抓屏幕,PrintWindow 与 BitBlt 各取一张,用颜色丰富度自动择优。
  2. 需要「不被采集」的 UI,用 SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE),并用像素差做回归验证。
  3. 图片进模型前先在本地压缩到长边 1600 px 左右,服务端反正会缩到约 1300²、上限 1024 tokens。
  4. reasoning_content 与 content 必须分流,否则答案区会被内心独白淹没。
  5. 上下文从 URL 来,别让人手动描述;规则表比 prompt 里写十句「请理解我的场景」有效。
  6. 凭据只写引用;进程内缓存解析结果,别让密码库成为请求路径上的同步阻塞点。

代码量并不大:宿主机约 1500 行 Python(抓屏 + 本地桥 + 流式客户端 + 浮窗),浏览器侧不到 400 行 JS。真正花时间的不是模型调用,而是「怎么把屏幕上的东西准确、干净、可复现地交到模型手里」——这一层做扎实了,剩下的事模型自己会做。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。