
如果你的編碼代理一直跑在終端里屏幕對你來說就只是擺設。上周我改一個前端暗色主題的細節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結果一測就發現問題圖片太大請求體動輒幾 MBAPI 延遲高token 消耗也莫名其妙地上去了。所以后來我加了一個預處理步驟縮放最長邊壓到 1024 或 768。這個尺寸對大多數視覺識別的需求都夠了還能顯著減少 token。換編碼如果不是必須保留透明背景優先用 JPEG質量 80。PNG 的 base64 體積通常比 JPEG 大很多。裁剪如果只需要某塊區域提前裁掉無關區域信息更干凈。一個簡單的處理思路from PIL import Image def preprocess_screen(image_path, max_side1024, quality85): 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, qualityquality) 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 內容如下\nTEXT }注意一個問題不要每次調用都把這輪截圖永久留在上下文里。識屏結果應該是“當前這一輪”的臨時上下文用完就清理。否則幾百輪對話下來圖片的 token 累積會讓上下文爆炸成本也會失控。4. 在 deepseek harness 里把它接進去自定義工具而不是魔改 CLI4.1 先搞清 harness 暴露的擴展點hooks 還是 toolsdeepseek 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.routeocr / vision / both。screen.capture.maxSide最長邊。screen.capture.qualityJPEG 質量。screen.context.expire識屏結果保留幾輪。把這些參數從代碼里拿出來放進配置文件你才能把它復用到其他項目上。這才是插件真正“可復用”的樣子。7. 收尾工具的價值不只是“看見屏幕”做這個識屏插件最大的收獲并不是“我的 agent 能看見屏幕了”。真正讓我想明白的是另一件事agent 工具的進化方向不是越來越像人而是越來越能補上工作流里斷掉的環節。一個能改代碼的代理很好但它再強也看不到你屏幕上的報錯彈窗一個能調 API 的代理很好但它讀不到設計稿的排版偏差。識屏插件解決的不是“視覺能力”而是“輸入通道”。這件事反映出來的趨勢更值得關注deepseek harness 這類本地編碼代理正在把 agent 從“一個固定的黑盒助手”變成“一條你可以自己改造的工作流基礎設施”。今天你可以給它加識屏明天你可以給它加語音輸入、加定時任務、加瀏覽器控制、加自定義工具。它的邊界不是官方功能列表而是你對工作流的理解。我現在的建議很具體如果你也一直在用編碼代理先不要急著寫復雜插件。找一個小到不能再小的痛點比如“每次跑完前端都想讓 agent 看一眼頁面”從一條截圖命令開始把鏈路跑通再說。識屏這條路最難的不是截圖不是 OCR也不是拼裝上下文而是你終于意識到工作流斷了的地方才是工具最該生長的地方。