如果你的编码代理一直跑在终端里,屏幕对你来说就只是摆设。上周我改一个前端暗色主题的细节,deepseek harness 在终端里跑得很顺,代码改完、测试通过,但它没法告诉我按钮在暗色模式下到底好不好看。于是我做了一件有点“野”的事:给这套 harness 写了一个识屏插件,让它能在处理任务前先截一张当前屏幕,把看到的内容变成上下文。
这件事做完以后,我发现问题不在于“AI 能不能看图”,而在于一套原本只处理文本的工作流,需要怎样接入视觉信息。识屏插件的价值,不是给 agent 多一只眼睛,而是把现实屏幕上的信息重新拉回文本和工具调用的循环里。表面看是一个小工具,背后涉及模型能力、上下文拼装、本地代理、错误排查和工程化边界。这篇记录一下,一个像 catch 一样的插件是怎么被一步步磨出来的。
1. 为什么缺“识屏”这个能力,会让本地编码代理变得不完整
1.1 agent 只能看到文本,屏幕是另一个世界
先说一个很容易被忽略的事实:deepseek harness 这类编码代理,本质上生活在一个纯文本世界里。
它能看到什么?仓库文件、终端输出、测试报告、日志、你输入的指令。它看不到什么?浏览器渲染出来的真实界面、弹出来的报错窗口、设计稿的排版、IDE 的高亮颜色。对 agent 来说,这些都不存在。
这不是模型能力的问题,而是输入通道的问题。OpenAI 那套 API 格式、Anthropic 的 messages 结构、DeepSeek 的 OpenAI 兼容接口,核心都是文本 token 的交换。你可以给模型传图片,前提是模型本身支持视觉输入,并且你的调用方真正把图片放进了正确的字段。大部分编码代理默认不会自动截屏,也不会把屏幕截图塞进请求里。
所以,当 agent 说“改完了”,而你需要确认页面效果时,它只能告诉你它改了哪些代码,无法告诉你页面看起来怎么样。你问它“你看一下屏幕”,它只能回你一个礼貌的抱歉。
这个缺口看似很小,但一旦你经常做前端、爬虫、自动化验收、界面 bug 修复,就会变成每天都要踩的坑:agent 的工作链路里缺的不只是眼睛,而是“屏幕上下文”这个入口。
1.2 deepseek harness 的核心工作流是一套本地执行链路
要理解识屏插件为什么可行,先要理解 deepseek harness 到底是什么。
它不是一个魔术盒,也不是 DeepSeek 官方发布的一个大型桌面应用。更准确地说,它是一套社区里常见的编码代理方案:以开源 CLI 代理为骨架,把模型 Provider 配置成 DeepSeek,通过本地代理或者配置切换,让终端里的 agent 用 DeepSeek 模型驱动,完成读仓库、改代码、跑测试、提交改动这类工程任务。
很多人喜欢把它理解成“Codex 接入 DeepSeek”的一种玩法。Codex 本身是 OpenAI 的编码代理形态,但作为开源组件,它的模型 Provider 是可以替换的。deepseek harness 就是这套替换实践的产物:用 DeepSeek 的 API 作为大脑,用本地 CLI 作为手和脚。
这个方案的价值有几个层面:
- 成本:DeepSeek 比常见海外模型便宜太多,适合长时间挂着让 agent 反复改代码。
- 本地可控:它的配置、日志、请求链路都在本地,你能看到每一轮请求到底发了什么。
- 模型可换:harness 这个英文词本身就有“套具、控制装置”的意思,在 agent 语境里,它定义的是执行循环:感知、决策、行动、观察。
但正是“本地可控”这个优点,让插件化成为可能。如果它是一个黑盒 SaaS,你很难在请求发出前插入一个截图步骤。而 deepseek harness 这类方案,通常允许你在调用链路的某个环节塞自己的逻辑。
这也就解释了为什么识屏插件值得做:因为你控制的不是一个远程服务,而是一条本地执行链路。
1.3 识屏解决的不是“截图”,而是上下文断裂
好,现在把问题说透。
假设你正在做一个前端页面还原。你给 deepseek harness 发指令:
按这份设计稿把登录页的间距调一下。设计稿是一张图。agent 如果只处理文本,它就没有“看到”这张图;如果你把图片路径给它,它可能会尝试用文件读取工具读图片,但大多数纯文本模型会把图片当成二进制数据,读不出语义。
再比如,你在跑一个桌面应用,程序弹了一个错误窗口,这个窗口的内容不在终端输出里,不在日志文件里,只在屏幕上。agent 看到测试失败了,却看不到错误弹窗。你手动把文字敲给它,效率就下来了。
这些场景的共同点是什么?工作流中间断了一截。人类是通过屏幕感知世界的,而 agent 是通过文本感知世界的。识屏插件做的事情,就是把屏幕上的内容重新转换成 agent 能消费的输入,无论是文字还是图片。
所以识屏插件真正的价值,不是“给 AI 加一双眼睛”,而是把被断开的上下文重新接回 agent 的感知循环里。
明白了这一点,你才能理解后面每一步实现选择——为什么不能直接截个大图丢给它?为什么要在 OCR 和视觉模型之间做取舍?为什么要在 harness 的扩展点里注册工具,而不是魔改源码?都是为了让这条上下文链路稳定、可控、可维护。
2. 先从一次失败尝试说起:识屏插件不是简单截个图
2.1 第一版实现:截图、编码、塞进消息
刚开始我很天真。我的想法非常简单:注册一个get_screen_info工具,让 agent 在需要的时候调用它,然后截屏、保存、读成 base64、塞进 user 消息的image_url字段。这不就完了吗?
第一版代码长这样:
import { execFileSync } from 'node:child_process'; import { readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; function captureScreen() { const file = path.join(tmpdir(), `harness-screen-${Date.now()}.png`); if (process.platform === 'darwin') { execFileSync('screencapture', ['-x', file]); } else if (process.platform === 'win32') { // Windows 走 PowerShell 截屏 } else { // Linux 尝试 gnome-screenshot 或 grim } return file; } function imageToBase64(file) { return readFileSync(file).toString('base64'); }然后我把它接进工具调用:截图 → base64 → 塞进消息 → 调模型。看起来逻辑完整。如果用的模型本身支持视觉输入,这一步通常就能跑通。
但问题恰恰出在“通常”这两个字上。
2.2 撞上的问题:thinking 模式必须回传 reasoning_content
第一次完整运行,直接 400。
错误长这样:
cc switch local proxy failed while handling codex endpoint /responses. provider: deepseek; upstream_status: http 400; cause: the `reasoning_content` in the thinking mode must be passed back to the api.看到这个报错的瞬间,我的第一反应是“代理挂了”,但仔细看cause那一段,问题其实出在多轮消息结构上。
DeepSeek 的推理模型在工作时,会返回一个reasoning_content字段,里面是模型自己的思考过程。如果你用 thinking 模式,下一轮请求时,这个reasoning_content必须被原样带回给 API,否则 API 会视为非法请求。
而我在识屏插件里做的事情,本质上是在“上一次模型回复”之后,插入了新的 user 消息,里面带着截图。问题就出在:我在拼装新消息时,没有把上一轮的reasoning_content正确保留,或者在某个版本里,插件把旧的 assistant 回复重新组装,但漏掉了这个字段。
这种错误最讨厌的地方在于,它不是识屏逻辑本身的错误,而是上下文拼装环节踩到了模型的服务端约束。
后来我反复测试发现,这类“thinking mode 必须回传 reasoning_content”的要求,在推理类模型里不算少见。只要你的 harness 允许自定义上下文修改,就很容易踩到这个坑。
2.3 排查链路:先用最小请求确认再扩大改造
那次失败以后,我做了一个很机械但很有用的排查。顺序是:
- 去掉识屏插件,恢复 harness 的默认调用。确认默认链路没问题。
- 单独用脚本调 DeepSeek API,把上一轮的
reasoning_content原样带回,确认 API 可以通过。 - 用调试面板看实际请求体,对比“默认请求”和“接了识屏插件之后的请求”,找出消息结构到底差在哪。
- 一点一点加回插件的逻辑,直到定位到是某个字段被覆盖。
如果你也遇到类似的 400,按这个顺序走通常能很快定位。不要一开始就在插件里改来改去,那样很容易把问题绕晕。
还有一点很重要:不要直接去改 harness 核心代码。一旦你改了源码,下次更新工具就会冲突,而且你很难判断是官方逻辑的问题还是自己改动的问题。插件的价值是“可插拔”,不是“改得越深越好”。
我知道很多人会觉得“看见屏幕”这件事很惊艳,但真正让它可落地的,恰恰是这些不惊艳的排查细节。
3. 一个可落地的识屏插件结构:抓屏、压缩、OCR/编码、拼装上下文
3.1 抓屏:跨平台的几种通用方式
识别屏幕的第一步,是拿到屏幕图像。这里没有统一标准,不同操作系统有不同命令。
macOS 最简单:
screencapture -x /tmp/harness-screen.pngWindows 上一般用 PowerShell,示例结构大概是:
Add-Type -AssemblyName System.Windows.Forms,System.Drawing $b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds $bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height $g = [System.Drawing.Graphics]::FromImage($bmp) $g.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size) $bmp.Save("$env:TEMP\harness-screen.png")Linux 桌面环境比较碎,常见的是gnome-screenshot或者grim,视你用的桌面环境而定。
在 Node 里封装一层,很容易做成平台无关的调用:
import { execFileSync } from 'node:child_process'; import path from 'node:path'; import os from 'node:os'; export function captureScreen() { const file = path.join(os.tmpdir(), `harness-screen-${Date.now()}.png`); if (process.platform === 'darwin') { execFileSync('screencapture', ['-x', file]); } else if (process.platform === 'win32') { execFileSync('powershell', ['-File', path.join(__dirname, 'capture.ps1'), file]); } else { execFileSync('gnome-screenshot', ['-f', file]); } return file; }如果只需要截某个窗口,可以进一步限定窗口 ID 或坐标区域。这个我没有在插件里做得太复杂,但设计上应该留出参数。
注意:截屏属于敏感操作。插件默认只截全屏可能是最省事的,但最负责任的做法是让用户配置“截图区域”,而不是什么都抓。
3.2 图像预处理:为什么不能直接塞原图
第一版我直接拿 4K 截图转 base64,结果一测就发现问题:图片太大,请求体动辄几 MB,API 延迟高,token 消耗也莫名其妙地上去了。
所以后来我加了一个预处理步骤:
- 缩放:最长边压到 1024 或 768。这个尺寸对大多数视觉识别的需求都够了,还能显著减少 token。
- 换编码:如果不是必须保留透明背景,优先用 JPEG,质量 80。PNG 的 base64 体积通常比 JPEG 大很多。
- 裁剪:如果只需要某块区域,提前裁掉无关区域,信息更干净。
一个简单的处理思路:
from PIL import Image def preprocess_screen(image_path, max_side=1024, quality=85): img = Image.open(image_path) img.thumbnail((max_side, max_side)) if img.mode in ("RGBA", "P"): img = img.convert("RGB") output_path = image_path.replace(".png", ".jpg") img.save(output_path, "JPEG", quality=quality) return output_path预处理的核心不是“压缩”,而是让模型看到它真正需要的信息,同时不把无关像素浪费在 token 里。
3.3 两条喂图路线:多模态直读 vs OCR 转文本
做完预处理之后,就要决定把什么内容喂给模型。这里有两套路线,选择取决于你的模型是否支持视觉输入。
| 路线 | 输入形式 | 优点 | 局限 | 适合场景 |
|---|---|---|---|---|
| 多模态直读 | image_urlbase64 | 模型能看到布局、颜色、文字、图片细节 | 要求模型支持视觉输入,token/延迟较高 | 前端还原、视觉验收、设计图比对 |
| OCR 转文本 | 纯文本块 | 成本低、兼容普通文本模型、上下文稳定 | 丢失布局和颜色,识别可能有误差 | 报错弹窗、终端输出、文档信息提取 |
我建议的做法是优先走 OCR 转文本,因为 deepseek 主流文本模型便宜、快、稳定。OCR 后的文字能直接塞进系统提示词或 user 消息,不依赖视觉能力。
但如果你确实需要让 agent“看懂”页面布局,比如让它判断按钮是否居中、卡片间距是否一致,那 OCR 不够,必须走视觉模型直读。
一个折中方案是把两种模式都做成插件选项:
route: "ocr":默认,抓屏 → OCR → 返回文本。route: "vision":抓屏 → 压缩编码 → 返回 image_url。route: "both":两个都返回。
这样你在不同任务里可以自由切换,而不用改插件代码。
3.4 上下文拼装示例
无论走哪条路,最终都要把结果拼进模型请求。
如果模型支持多模态输入,标准结构是:
{ "role": "user", "content": [ { "type": "text", "text": "这是当前屏幕截图,请根据看到的内容继续处理。" }, { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,<BASE64>" } } ] }如果走 OCR 文本,就非常简单:
{ "role": "user", "content": "当前屏幕 OCR 内容如下:\n<TEXT>" }注意一个问题:不要每次调用都把这轮截图永久留在上下文里。识屏结果应该是“当前这一轮”的临时上下文,用完就清理。否则几百轮对话下来,图片的 token 累积会让上下文爆炸,成本也会失控。
4. 在 deepseek harness 里把它接进去:自定义工具而不是魔改 CLI
4.1 先搞清 harness 暴露的扩展点:hooks 还是 tools
deepseek harness 这类工具,通常不会提供一个“插件市场”让你一键安装。它的扩展点一般有两种:
- hooks:在特定时机执行脚本,比如每次请求前、响应后。
- tools:向 agent 注册一个可被调用的外部工具函数。
这两种方式各有适用场景。hooks 更像“监听器”,适合做日志、拦截、注入环境信息;tools 更像“手”,适合做实际动作。
我最后选择的是 tools。原因很简单:我不希望每次请求都强制截屏,而是希望模型在需要时主动去调用截屏工具。
在系统提示词里加一句规则:
当用户提到屏幕、界面、报错弹窗、页面效果、视觉验证时,先调用 get_screen_info 工具,再回答问题。这样模型自己会决定什么时候看屏幕,而不是每轮都盲截一张图,浪费 token 和时间。
4.2 一个通用接入方式:作为外部工具注册
具体到实现,我在自己的插件里注册了一个外部工具。大致结构是这样:
const screenTool = { name: "get_screen_info", description: "Capture the current screen and return OCR text or image. Use when user asks about screen, UI, popup, page preview.", parameters: { type: "object", properties: { route: { type: "string", enum: ["ocr", "vision", "both"], default: "ocr" } }, required: [] }, async run({ route }) { const imagePath = captureScreen(); const processedPath = preprocess(imagePath); if (route === "vision" || route === "both") { const base64 = imageToBase64(processedPath); return { image_base64: base64, ...(route === "both" ? { ocr_text: ocr(processedPath) } : {}) }; } return { ocr_text: ocr(processedPath) }; } };具体注册位置在不同版本里不一样,有的在 plugin 目录,有的在配置文件里声明 tools。落地前请先看对应版本的 README 或--help,不要照抄网上的旧代码。
这里要特别说明:这段代码不是某款工具的官方 API,而是一个比较通用的工具注册结构。它的意义在于帮你理解“识屏插件在 harness 里的定位”,而不是告诉你某个具体版本应该怎么写。
4.3 加一个本地调试面板:用 dsh web 看每一轮请求
写完插件之后,我开始频繁使用一个本地调试面板。在我用的方案里,入口是pnpm dsh web。它会把每一轮请求的输入、输出、耗时、报错都显示出来。
这个面板对我的帮助特别大。因为当你把屏幕信息插入到请求里时,你非常需要确认一件事:截图内容到底有没有被正确送到模型那边。
如果模型返回 400,你可以在面板里看请求体。对比“加了插件”和“没加插件”的请求,很快就能发现是哪个字段被覆盖了,是reasoning_content丢了,还是messages顺序乱了。
如果你用的 harness 没有类似面板,至少要在插件里保留请求日志。
logs/ screen-tool.log request-body.log errors.log不要小看日志。识屏插件一旦跑起来,失败模式比普通工具复杂得多——截图可能失败,OCR 可能返回空,API 可能 400,请求体可能因为上下文过长被截断。没有日志,你只能瞎猜。
5. 识屏插件真正适合的场景和不适合的场景
5.1 已经验证有效的几个场景
第一个场景是前端代码修改后的自检。以前 agent 改完样式,我只能心累地看。现在它改完之后可以自己调用识屏工具,截一张浏览器里的实际渲染效果,然后判断“间距是否合理”“是否居中了”“是否被遮挡”。如果用了视觉模型,它甚至能直接指出哪里不对。
第二个场景是读取系统级报错弹窗。很多桌面端工具的报错并不进入终端,而是弹在屏幕上层。普通测试输出看不到,日志里也可能没有。识屏插件通过 OCR 把弹窗文字提取出来,agent 就能准确理解错误内容。
第三个场景是用设计图指导编码。如果你截一张设计稿给 agent,并配上视觉模型,它可以根据屏幕上的设计稿来调页面。这个场景对多模态支持的要求比较高,但对前端开发的吸引力也最大。
5.2 不建议做的场景
识屏插件不是万能的。以下场景我明确不建议使用:
| 场景 | 为什么不建议 |
|---|---|
| 实时连续桌面监控 | 截图频率一高,token 和延迟直接爆炸,且安全风险不可控 |
| 高权限交互界面自动化 | 一旦 agent 看到敏感内容,可能误触发危险操作 |
| 纯文本任务 | 强行识屏只会增加延迟和错误率,没有收益 |
| 银行、密码、内部系统页面 | 截图内容可能包含敏感数据,不建议发给任何外部模型服务 |
| 屏幕内容转发到不可信服务 | 如果插件把截图上传到你无法控制的 API 或存储,风险很高 |
这里做一个原则性表格,比参数更重要:
| 判断问题 | 通过标准 |
|---|---|
| 这个信息能通过文本拿到吗? | 能,则不要截屏 |
| 任务是一轮一轮执行的吗? | 是,识屏合适;需要连续流,不合适 |
| 截图内容允许发到当前模型服务端吗? | 不允许,则只 OCR 本地处理,或不做 |
| 模型支持视觉输入或 OCR 够用? | 都不支持,识屏无意义 |
5.3 一个简单的判断框架:要不要做识屏,先回答三个问题
我后来总结出三个问题,每次想给 harness 加识屏能力时,先自问一遍:
- 这个信息能不能先从文本拿到?如果可以,优先文本。截图是最后手段。
- 任务是不是快照式的?识屏适合“看一眼当前状态,然后做判断”的场景,不适合持续追踪动态画面。
- 你承担得起截图带来的上下文和隐私成本吗?如果截图内容敏感,或模型不支持视觉,就要换方案。
这三个问题能过滤掉至少一半“想给 harness 加识屏”的冲动。它不是每时每刻都需要,但当你需要的时候,它是唯一能接上上下文的方式。
6. 如果你也想写自己的 harness 插件,建议按这个顺序来
6.1 先用默认配置跑通最小任务
不要一上来就写识屏插件。
先确认你的 harness 环境本身没问题:模型配置正确,API 能连通,README里的最小示例能跑通。比如让 agent 读一个文件,改一个函数,跑一次测试。
这个步骤看起来多余,但它能隔离问题。如果你在最开始就引入插件,遇到一个 400,你很难判断是插件的问题还是环境的问题。只有默认链路稳定了,你才有资格谈扩展。
6.2 用一条样例确认输入通道
不要直接把插件完整写完。先把“截图”这一步单独跑一遍,把“OCR”这一步单独跑一遍,把“调用 API”这一步单独跑一遍。
我的流程是:
- 先手动截一张图,确认文件存在。
- 再手动跑一次 OCR,确认输出文本正确。
- 用脚本构造一个最小请求,把 OCR 文本发给模型,确认返回正常。
- 最后才把这三步串进 harness 的工具调用链。
每个环节单独验证,能让你在后续排查时立刻知道是哪一环坏了。
6.3 识别扩展机制,而不是抄一堆社区片段
deepseek harness 这类工具更新很快,不同版本的 hooks 和 tools 接口可能完全不同。如果你在网上看到一个旧插件片段,不要直接复制进去。先做三件事:
- 打开本地安装后的源码或类型定义,找到工具注册入口。
- 看示例配置里有没有声明自定义 tools 的地方。
- 用最小改动跑通一个“hello world”工具,比如返回当前时间,然后再替换成识屏逻辑。
很多人的插件问题不是逻辑写错了,而是注册方式不对。这一点比识屏本身更重要。
6.4 日志、错误重试、上下文管理是长期使用门槛
工具能跑起来和能长期使用,完全是两回事。识屏插件一旦放进日常开发流,就会遇到各种脏场景:
- 截图命令在某些环境被权限拦截。
- OCR 在低分辨率下返回空字符串。
- API 因为上下文中包含图片而产生更高的 token 成本。
- 上一轮截图内容没有清理,导致后续请求越来越大。
所以我建议至少在插件里做到:
# 每次抓屏后保留原始文件路径 # 每次 OCR 后写入 result 到日志 # API 400 时记录 request body 的前 200 个字符 # 上下文清理策略:识屏结果只在当前轮有效当一个工具同时具备“干净的输入、可观测的执行过程、明确的失败反馈、可清理的副作用”时,它才算从实验 hack 变成了真正的工程工具。
6.5 从一次性插件走向可复用工具
最后一步是参数化。
不要把你的截图区域写死,不要把你的输出模式写死。用配置控制:
screen.capture.area:全屏还是指定区域。screen.capture.route:ocr / vision / both。screen.capture.maxSide:最长边。screen.capture.quality:JPEG 质量。screen.context.expire:识屏结果保留几轮。
把这些参数从代码里拿出来,放进配置文件,你才能把它复用到其他项目上。这才是插件真正“可复用”的样子。
7. 收尾:工具的价值,不只是“看见屏幕”
做这个识屏插件最大的收获,并不是“我的 agent 能看见屏幕了”。
真正让我想明白的是另一件事:agent 工具的进化方向,不是越来越像人,而是越来越能补上工作流里断掉的环节。一个能改代码的代理很好,但它再强,也看不到你屏幕上的报错弹窗;一个能调 API 的代理很好,但它读不到设计稿的排版偏差。识屏插件解决的不是“视觉能力”,而是“输入通道”。
这件事反映出来的趋势更值得关注:deepseek harness 这类本地编码代理,正在把 agent 从“一个固定的黑盒助手”变成“一条你可以自己改造的工作流基础设施”。今天你可以给它加识屏,明天你可以给它加语音输入、加定时任务、加浏览器控制、加自定义工具。它的边界不是官方功能列表,而是你对工作流的理解。
我现在的建议很具体:如果你也一直在用编码代理,先不要急着写复杂插件。找一个小到不能再小的痛点,比如“每次跑完前端都想让 agent 看一眼页面”,从一条截图命令开始,把链路跑通再说。
识屏这条路,最难的不是截图,不是 OCR,也不是拼装上下文,而是你终于意识到:工作流断了的地方,才是工具最该生长的地方。