安装
在 DeepSeek Harness 里通过 dsh-market 安装
dsh plugin --profile web add dshmarket
或使用命令行
dsh plugin --profile web add github:davidekingsss/dsh-screen-eye
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络。请先审阅源码,并尽量锁定 commit(github:owner/repo#sha)。
截图
README
给 macOS 与 Windows 上的 DeepSeek Harness 装一只自主的眼睛:agent 截屏后在同一次工具调用里直接拿到图,于是它能自己去看正在运行的应用、弹窗、报错,或者自己刚写的界面——不必再让你手动截图。
无原生构建步骤、不带预编译二进制、零依赖。
它提供什么
两个模型可调用的工具:
| 工具 | 作用 |
|---|---|
screenshot |
截屏,并把图片本身作为 image 内容块返回给模型——模型是真的看得见。 |
screen_permission |
在 macOS 上报告本进程当前是否允许截屏;用 action: "guide" 可直接打开对应的系统设置面板,并指出该打开列表里的哪一项。Windows 没有这项权限可报告,因此该工具在 Windows 上不注册。 |
mode 决定截什么:screen(默认,主显示器)、display(按序号指定某一块
屏幕)、region(指定矩形,原点是主屏左上角,因此在主屏左侧或上方的显示器
取负坐标)、displays(不截图,只列出已连接的屏幕,以及 display 需要的序号
和每块屏幕在 Windows 上的起点坐标),以及需要人交互的 window / select
(等待用户点选窗口或拖拽出选区)。Windows 没有系统级选区工具,所以在那里
select 会被明确拒绝并说明原因,而 window 指的是当前在最前面的窗口。
把 frames 设为大于 1,一次调用就会按 interval_ms 的间隔连拍那么多张并全部返回,
这是"看清随时间变化的过程"的方式。它刻意不是 GIF:harness 以单帧存储图片,
动画 GIF 到达模型时只剩第一帧。
这两个旋钮都交由你调,而"有用的方向"不一定是调细。采集速度取决于面积——
整块 4K 屏要 155ms,而 1200×800 的区域只要 56ms——所以要看清一个几百毫秒的
组件动画,正确做法是截它所在的那一小块,而不是要求整屏用更细的间隔。
返回里会报告实际达成的间隔,达不到时明确说明。
docs/motion.md 有实测数据与推理。
决定成本与分辨率的那个旋钮是 interval_ms:它决定运动被采样得多细,而帧数
就是一次连拍的全部花费。这里没有任何东西强加一段固定时长——连拍的长度是
(frames - 1) × interval_ms,而调用若先等待画面变化,结束时机就由画面决定——
所以唯一的封顶是 timeout_ms(整次调用的预算,默认五分钟,可按次调高)。
帧数上限是 provider 自己的"单请求 600 张图",不是本插件拍的数字:每帧是一张图、
最多 384 个视觉 token(实测约 380),因此帧数是一个成本决策,交给看得见上下文的
调用方比交给插件更合理,工具描述里也把这道算术直接写了出来。
其余情况下每次调用只返回一张图。这是刻意的约束,而不是系统的限制:screencapture
是「每块屏幕写一个文件」,所以在多显示器 Mac 上不加限定的截图会产出多个文件,
而本插件的管线只解析并读取一个路径——其余的会以本插件没有选择过的文件名留在
盘上。因此默认被钉在单块显示器上,要看另一块就用 display。
为什么需要这个插件
多数截图工具假设难点在于「取像素」。在 macOS 上难点是权限,而且它失败 的样子像 bug:
screencapture: could not create image from display
macOS 把截屏能力锁在「屏幕录制」权限后面,而该权限挂在责任进程上——也就是 macOS 认为要对整棵进程树负责的那个应用。
这里的关键是有没有一个可归属的 App 身份,它决定了两条完全不同的路径:
- 宿主是 App 时(例如 DSH 桌面客户端):macOS 会弹一次系统弹窗问用户。 但弹窗只负责把 App 加进列表,加进去之后开关默认是关的——所以此时截图 仍然失败,用户必须手动打开那个开关。
- 宿主没有 App 身份时(应用内插件市场通过 detached 辅助进程重启宿主,宿主被挂到
launchd之下、其上方没有任何应用):macOS 无法归属请求,于是静默拒绝, 连弹窗都不会出现。
两种情况下都只有一条出路:让那个开关处于打开状态。而这个权限无法用程序授予
——TCC 数据库受 SIP 保护,tccutil 只能重置。所以本插件只做真正做得到的事:
- 检测:真的去截一次并按结果分类,而不是靠启发式猜测;
- 说清该找哪一行:给出「条目名随启动方式变化」这条原则和常见情况——因为授权挂在 哪一行取决于 DSH 怎么启动的,进程内部算不出来(见下文表格);
- 打开面板:按需直接跳转到系统设置里对应的那一页;
- 把步骤当作工具结果返回:于是 agent 交给你的是一份修复指引,而不是一段 报错,并且明确说明中途不要重试。
Windows 没有这道闸门,也就没有这段故事:那里的难点是截图可能成功但没用—— 进程若不感知 DPI,拿到的就是屏幕的降采样副本;若没挂在交互式桌面上,拿到的 就是一张黑图。两者都在下面的 Windows 一节里被解决。
安装
dsh plugin --profile web add github:davidekingsss/dsh-screen-eye
# 然后重启 dsh
dsh plugin --profile web add github:davidekingsss/dsh-screen-eye
# 然后重启 dsh
macOS 与 Windows 是同一条安装命令,平台层自己挑引擎。
若用本地检出目录,而不是已发布的源:
dsh plugin --profile web add -w link:/path/to/dsh-screen-eye
本插件没有构建步骤、也没有自身依赖,安装期不编译任何东西。macOS 上会有一个小助手 在首次截图时从插件随附的源码编译——见实现。
授予屏幕录制权限(macOS,一次性)
调用一次 screenshot。如果缺权限,返回结果会明确告诉你该怎么做;
screen_permission 配合 action: "guide" 会走完整个引导:先检查,只在授权确实缺失时
才打开面板,并给出该找的条目名。
三种状态,只有一种叫「从未授权」
我们在 macOS 26.6.2 上把这三种都制造出来实测过,因为它们的表现完全不同:
| 状态 | 系统弹窗 | 是否在列表里 | 能截图吗 |
|---|---|---|---|
| 从未申请过 | 会弹,首次请求时 | 用户点进设置后就有了 | ❌ |
| 曾授权、后来关掉 | 不弹 | 在,开关是关的 | ❌ |
| 从列表里删掉 | 会弹,下次请求时 | 用户点进设置后就有了 | ❌ |
条目名取决于「你用哪个 App 启动了 DSH」
macOS 是按发起请求的 App 来命名条目的,不是按 harness 本身。同一台机器上实测到三种:
| 启动方式 | 列表里的条目名 |
|---|---|
快捷指令(运行Deepseek Harness.app) |
运行 Deepseek Harness |
| 浏览器 | Google Chrome |
| 终端 | Terminal / iTerm |
所以没有唯一正确的名字可以打印——引导给出的是「你用来启动 DSH 的那个 App」这条原则, 加上常见候选。你要做的只是在列表里对上号。
那个「
运行Deepseek Harness.app」其实是一个 macOS 快捷指令的 droplet 容器 (CFBundleName为ShortcutDroplet),快捷指令把 harness 以 detached 方式拉起后自身退出, 所以进程链上看不到它——名字只存在于系统记录的归属里。
真实流程:四步,中途不要重试
- 留意系统弹窗:「…想要录制此电脑的屏幕和音频。」若出现,点 「打开系统设置」 ——它会自动把这个 App 加进列表。若没出现(从终端启动、或曾被拒绝过),自己打开 系统设置 → 隐私与安全性 → 屏幕与系统音频录制。
- 在列表里找到你启动 DSH 所用的那个 App 对应的条目(名字见上表)。
- 把它的开关打开。 ⚠️ 系统自动加入的条目默认是关闭的, 「在列表里」不等于「已授权」。这一步只能由人来做。
- 然后重新调用工具。不需要重启 DSH——授权对下一次截图即生效。
为什么第 3 步必须写出来:在第 1 步和第 3 步之间做任何判断或重试都是错的—— 此时截图必然以同样的方式失败,而那不是故障,是流程还没走完。
macOS 可能定期要求重新确认此权限,把同一个条目的开关重新打开即可。
Windows 完全不需要这些,见 Windows。
配置
所有键都是可选的——而且它们全都可以在 harness 自己的设置页里改:设置 →
Screen Eye(左侧独立一项,眼睛图标),或者直接手改 ~/.dsh/settings.yaml 里的
screen-eye: 一节。两种方式都是下一次调用即生效、不需要重启,并且插件的挂载
条目始终是"清空某个字段后回落到的那一层"。三层关系、这个页面的形态、以及它背后的
两条平台约束见 docs/settings.md。
| 键 | 默认值 | 含义 |
|---|---|---|
outputDir |
<图片文件夹>/Screen Eye |
截图 PNG 的落盘目录——系统图片文件夹,并单独放一个子目录,免得五十张截图混进你自己的照片里。 |
locale |
en |
引导文案语言:en 或 zh。 |
timeoutMs |
300000 |
一整次调用的预算(含 wait_for_change 的等待)。等待没用掉的时间才归后面的采集。 |
frames / interval_ms |
1 / 200 |
每次调用的帧数与帧间目标间隔。上限 600,这是 provider 对单请求图片数的限制,不是本插件的策略——超过的部分会被截下来然后替换成占位文本。每帧是一张图、最多 384 个视觉 token(实测约 380),所以帧数是一个成本决策,由你来做。 |
duration_ms |
— | 直接说明运动持续多久,由工具自己定帧数与间隔,而不必你手算。 |
frames、interval_ms、duration_ms 描述的是同一次连拍,任意两个确定第三个:
帧数 + 间隔给出跨度,帧数 + 时长给出等分该窗口的间隔,间隔 + 时长给出能放几帧。
三个同时给出会被拒绝而不是替你裁决。
这也正是成本杠杆:每帧都是一张图,而图就是成本。窗口不变而调大
interval_ms,就是用更少的图看同一段运动——分辨率换成本;调小则相反。
只给时长时,按常规帧数采样而非上限,因为上限是花钱最多的答案,而你没要它;
但如果这次调用先等待画面变化,结束时机就由画面决定,帧数这时是余量而不是计划。
wait_for_change: true 同时确定了连拍何时结束:画面不再变化时它自己收尾,
于是 frames 变成上限,返回值会说明是哪种结束方式。wait_timeout_ms(默认
30000)是等待运动开始的时间上限。until_still: false 可以退出这一行为,改为拍满
整个窗口——那是"要一段跨度"而不是"要一个事件"时该做的选择。
| maxDimension | 4096 | 截图允许的最大单边像素。4096 是 provider 在"单请求含 15 张及以上图片"时对单边的限制,而一次连拍可以带上百张。超限的截图会被拒绝并报出真实尺寸,而不是缩放到上限——所以要整屏截取更大的显示器,得把这里调高。 |
| keepRecent | 50 | outputDir 里保留的最新截图数量。一张截图是几 MB 的 PNG,而用眼睛的 agent 会截很多张,所以这个目录默认是有上限的。设为 0 表示全部保留。 |
| requireImageCapableModel | true | 当调用方模型未声明图片输入时直接拒绝,而不是返回一张它看不见的图。 |
| deleteAfterCommit | false | 提交到附件存储后删除 PNG。默认关闭,以便返回的路径可再次读取。 |
| announceCapability | true | 在系统提示词里放一句常驻说明,告诉 agent 它能看屏幕,并注册 screen-eye 技能。关掉则工具照常定义但不作宣告。见让模型知道自己有眼睛。 |
提交到存储时会去掉那些会让它无法无损保存的记录。在 macOS 上就是去掉
screencapture 给每张截图附带的 ICC 描述文件、EXIF 与 iTXt 记录,而磁盘上的
文件保留它们:这几个记录正是附件存储"保留原字节还是重编码"的判定依据——带
元数据的图片不允许直通——所以去掉它们,截图才能按原字节存下,而不是被编码
成质量 85 的 WebP。在 Windows 上则没有东西可去:GDI+ 这几个记录一个都不写,
截图本来就已满足存储的直通条件。存储无论如何都会转成 sRGB,因此没有任何存活
下来的信息被丢掉;而你能打开的那个文件仍然带着它的色彩描述。
清理只会删除本插件自己写出的文件:outputDir 的直接子项、且文件名严格匹配
本插件生成的形状(shot-<时间戳>-<后缀>.png)的普通文件。它绝不递归、绝不动
其他命名规则的文件,也绝不会删掉刚刚返回给你的那一张。
# cordis.patch.yml
- insert:
- id: screen-eye
name: dsh-screen-eye
config:
locale: zh
outputDir: /Users/me/Pictures/agent-shots
keepRecent: 200
实现
screenshot 工具 ──▶ lib/capture.mjs ──▶ 平台引擎 ──▶ PNG
│ macOS:screencapture
│ Windows:PowerShell + System.Drawing
└──▶ attachments.saveImage() ──▶ image 内容块 ──▶ 模型
截图不会被缩放到 maxDimension 以内,这个上限也刻意没有设得更低,两者出自同一个实测:
harness 在模型看到图片之前,会先按路由的像素预算投影——默认 640,000 像素,16:9 屏幕约
1066×600——所以 4K、5K、6K、8K 显示器截出来的图,到达模型时是同一张。在这个预算之下,
把上限调小省不下一个 token;而在这里做缩放,会在"模型在图上量到的位置"与"region 需要的
屏幕坐标"之间再多插一级缩放,而那正是放大工作流所依赖的映射。
两个引擎都不自带二进制,各自在使用者的机器上构建。macOS 的正式引擎是系统自带的
screencapture(1)——Apple 签名、内部已使用 ScreenCaptureKit、无需任何构建;另有一个
常驻助手,由随插件发布的 engine.swift 在首次截图时编译一次,换取单次进程达不到的
速度(一次变化检测走助手 22.6ms,走二进制 55.8ms)。选择现场编译而不是随包发布,
和 Windows 那边的理由是同一个:预编译二进制意味着多架构构建、固定的部署目标,以及一个
每次重建都会变的 ad-hoc 签名哈希——哈希一变,用户的屏幕录制授权就静默失效。
Windows 走每一台机器都有的 Windows PowerShell 5.1,通过 Add-Type 在内存里临时编译
一层垫片来驱动 System.Drawing,一次调用结束即消失。
macOS 助手是优化项而非依赖:screencapture 始终是正式引擎,除授权拒绝外任何失败
都会回退到它,所以没有 Swift 工具链的机器截图行为与从前完全一致。
图片通过与内置 read_image 完全相同的附件通道抵达模型,因此其校验、降采样与
会话回放行为与任何其他图片一致。
让模型知道自己有眼睛
工具抵达模型有两条互不相通的通路:每次请求都会带的 schema,和系统提示词里的常驻说明。 schema 回答的是「这个工具怎么调」,而只有已经决定去查它的调用方才会读到;模型还在 决定做什么的时候,没有任何东西会读 schema。只带 schema 的插件,就是模型只能靠猜的插件。
这一点在本插件身上是实测出来的,不是推测。本机会话库全部 60 个会话中,54 个会话提到
screenshot 恰好一次,且全部是同一句——Web 界面的「the browser provides no implicit
DOM, route, or screenshot context」;而真正调用过该工具的只有 9 个会话,其中 6 个是开发
和测试本插件的会话:2 个在本仓库里,4 个在首个 commit 落地当天的临时目录里。另外 3 个
会话是拿它做自己的事。提示词里一片沉默,工具就没人去够。
所以插件会在它所描述的工具旁边注册一个 section:
This deployment can see the screen, and seeing is a way of finding things out rather than a last resort: when the answer is on screen rather than in a file, look instead of searching. Read text out of a running app's interface — a window title, a field value, a dialog, a message — by observing that app, whose accessibility tree hands it over as text; keep the screenshot tool for visual facts with no text to read, such as layout, colour, motion, and anything you are judging by eye. A window too small, collapsed, or covered to answer from is a reason to expand it, scroll it, or move it back into view, and then look — not a reason to go looking for the same information in caches, databases, files, or APIs. The screenshot tool captures and returns the picture in that same call, so no read_image step follows, and a burst of frames records motion instead. A capture can need Screen Recording permission; screen_permission reports whether it is granted and opens the pane that grants it.
开头几句路由说明不是第一版就有的,它们是第二次实测的产物。有人问「我现在在看什么视频」, 一次会话答对了,但代价是:16 条 shell 命令翻进某个 App 的缓存数据库、2 次调 Web API, 只为取回一个就印在窗口标题栏里的标题。当时的提示词从没说过「运行中的界面本身就是答案 所在的地方」——它写的是「答案在屏幕上而不在文件里」,模型把「在屏幕上」读成了「在图片里」。 所以现在两条通道被明确分开,第三种情况也写进去了:当界面本身是障碍时,改变界面是通向 答案的一步,不是绕路。
其中有四点是有意为之:
- 只在工具真的挂载成功时才注册。 没有附件存储的部署不会注册截图工具,而此时提示词 里若宣称可以截图,只会让模型去调一个返回空的东西。
- 授权那句是按平台问出来的。 Windows 不注册
screen_permission,也没有授权可报告, 所以那句授权说明在那里不存在。这个问题问的是引擎注册表,与apply()问的是同一个。 - 技能是长版本。
screen-eye技能承载两条通道的分工、为什么「已经在屏幕上显示的东西」 不该去翻缓存、模式对照表、缩放屏对坐标意味着什么,以及什么都拿不到时怎么办;它走运行时 注册,所以「安装插件」就是全部安装动作,也不存在一个会与描述它的代码脱节的技能文件。 announceCapability: false去掉常驻说明,而且可以改回来、无需重启:它的文本是每次 组装时求值的 provider,不是挂载时定死的字符串。技能注册无法撤回,所以那一个以挂载时的 设置为准。
保留工具、去掉常驻说明是一种受支持的部署方式,不是降级——如果你那边的说明文字读起来 不对,这就是那个开关。没有「连工具一起关掉」的开关,因为一个存在的意义就是让 agent 自己去调工具的插件,把它藏起来并不会让它变得更好。
平台支持
macOS 与 Windows,且在两处强制:bundle patch 带
disabled: !!js process.platform !== 'darwin' && process.platform !== 'win32',
其他平台连模块都不会被 import;apply() 再检查一次,使得绕过 patch 的直接
挂载也无法注册没有引擎的截图工具。
Windows
Windows 不需要授权也不需要引导——任何挂在交互式桌面上的进程都能截屏——所以
那里根本不注册 screen_permission,screenshot 就是全部工具面。与 macOS 真正
不同的有四点,每一点都是被解决的,而不是被写进文档绕过的;
docs/windows.md 里有实测数据。
- 缩放。 Windows PowerShell 默认是 DPI 不感知的,而 DPI 不感知的进程拿到的 是桌面的降采样副本:在 125% 缩放的 3840×2160 面板上,它报告并截取的是 3072×1728。引擎在读取任何东西之前先声明 per-monitor 感知,取到的是真实像素。
- 看不见的会话。 没有挂在交互式窗口站上的进程不会失败——
CopyFromScreen返回黑图,朴素实现会把它当成"成功截到一张黑屏"。引擎先检查窗口站与会话并 点名拒绝,而整帧全黑的照片会带着说明返回,说明这通常意味着什么。 - 连拍。 Windows 上单次截图冷启动约 380ms,且几乎与面积无关,因为每次调用都要付 一次 PowerShell 启动(其中 148ms 是它什么都不做时的启动成本)。现在引擎只启动一次 并常驻:C# 垫片每台机器只编译一次,之后单次截图只需 16-22ms。六帧连拍若拆成 六次冷调用,会把一段 400ms 的动画采样成两秒半,所以 Windows 把整段连拍放进同一个 引擎进程,计划里的间隔才成为可达的。
- 只播一次的动画。 组件过渡只播一次,而模型不可能在用户点击的那一瞬间发出调用
——实测:在一段 300ms 过渡刚开始时发出的连拍,两个平台都是 0 帧可用。
wait_for_change: true就是答案:"我盯着这块区域,画面一动就开始拍。"在一段已知的 300ms 过渡上实测:帧序列在动画开始后 103ms 起拍,8 帧中有 4-5 帧落在动画 区间内。macOS 有同样的选项,并且自 2026-09-16 起也有同类的常驻助手进程——它的一次 变化检测耗时 22.6ms,而走screencapture要 55.8ms;这决定了同一段 300ms 过渡是 只录到后三分之一,还是完整录下来(见docs/macos-findings.md)。 结束端同样交给画面判定:等待过变化的连拍,会在画面静止时自己收尾,所以frames是上限而不再是"猜动画有多长"。同一段过渡上实测:7 帧覆盖完整段动画后自行停止, 10 帧的额度还剩 3 帧没用。返回值会说明是哪种结束方式;想要"固定一段窗口"而不是 "一个事件"的调用方,用until_still: false明确要求。 - 双屏。 显示器按主屏优先列出并带各自的原点,所以第二块屏上的
region——包括位于主屏左侧、坐标为负的那块——都落在正确像素上。已在 3840×2160 主屏 加一块 x = -2560 的 2560×1600 屏上逐像素比对验证。 select。 Windows 没有系统级选区工具,所以该模式被明确拒绝并给出原因、 建议改用region;window截的是当前在最前面的窗口,因为同样没有可点的东西。
环境要求
- macOS 或 Windows,以及 harness 自身的 Node 运行时。两条截图链路都不需要额外 安装任何包;安装期不编译任何东西。macOS 上会在首次截图时从源码编译一个小助手, 而机器上没有 Swift 工具链时,插件不用它也能正常截图。
- macOS 上需要给运行 harness 的进程授予「屏幕录制」权限(见上文)。Windows 不需要。
- 一个声明了图片输入的模型路由。保持
requireImageCapableModel默认值时, 被声明为纯文本的路由会在事前被拒绝,并在消息里点名该模型,而不是悄悄截一张 它看不见的图。注意这个声明在 harness 的模型元数据里,而不在模型身上:settings.yaml可以覆盖内置的模型条目,一条只写了text的覆盖会让一个 明明看得见的模型被拒绝。拒绝消息会点名模型和那个设置项,因此修好它只需要 一行,而且不重启即可生效。
开发
npm install
node test/selftest.mjs
该测试套件无需启动 harness:逻辑模块被直接导入,工具定义经由桩上下文执行, 因此在一台从未装过 harness 的机器上同样能跑——CI 走的就是这条路,macOS 与 Windows 上各跑一遍。真正实拍的用例只在本机确实看得见自己的屏幕时运行—— macOS 上要有屏幕录制权限,Windows 上要有可见桌面——所以在授权之前套件依然是绿的。
针对某个平台自身模型的用例,直接问那个平台的模型,而不是问当前宿主机:于是 Windows 引擎生成的 PowerShell 在 macOS CI 上也会被断言,macOS 的权限文案在 Windows 上也会被断言。这是刻意的:只有当另一侧也被检查时,这条接缝才有意义。
插件导入的三个 @deepseek-ai/* 包在 devDependencies 里精确钉版本,这是
刻意的:这几个包把当前版本线发在 next dist-tag 下,而 latest 仍指向一个
老得多的版本,不钉版本就会装到旧的那一个、import 直接失败。
docs/verification.md 记录了实际跑过的内容——
两个平台上的自测、隔离 profile 中的加载器验收、以及端到端 agent 回合——并说明
每一项证明了什么、没证明什么。
docs/macos-debugging.md 是一份自包含的 Mac 端验证手册,
专门写成"可以直接交给一个没有任何上下文的 AI 去执行"的形式:按优先级排列的八个步骤,
每一步都写明怎么跑、期望看到什么、出现偏差意味着什么、要回报哪些数字。
tools/motion-fixture.html 是它要用的动画素材:每 4 秒自动播放一次的 300ms 过渡,
不需要有人在连拍等待时去点击。
docs/macos-findings.md 是 2026-09-16 在 Mac 上跑完这份手册的
实测记录。在信任别处任何 Mac 数字之前都值得先读它:它确认了 region 是像素级精确裁剪、
静止判定的前提成立;同时推翻了"macOS 的检测足够留下 300ms 过渡里的四五帧"这一说法
——实测只留下一到两帧,因为每次检测都要付一次完整的 screencapture 进程启动开销。
正是这个测量结果让 macOS 也补上了常驻引擎,也让本插件不再像以前说的那样"完全不含编译
产物":该助手进程在使用者的机器上从源码编译,继承屏幕录制授权而不是另外申请,
并且任何失败都会回退到 screencapture。
许可证
MIT
评论
评论存放在 GitHub Discussions。用 GitHub 账号登录后可发表评论或点表情。