dsh-screenshot-feedback-hook-mcp
已验证dsh-screenshot-feedback-hook-mcp · v0.2.0 · MIT · Web 界面
Screenshot feedback for DeepSeek Harness — let your coding agent SEE what it builds
安装
dsh plugin add dsh-screenshot-feedback-hook-mcp 用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。
源码
标签
作者
说明文档
dsh-screenshot-feedback-hook-mcp
English | 中文
一个 DeepSeek Harness 插件:让 agent 看到自己刚产出的真实画面 —— 前端页面、EasyEDA/CAD 工程图、任意桌面应用 —— 并据此自我纠正。
它是 screenshot-feedback-hook-mcp 的 dsh 那一半;截图与压缩仍然全部由 Python 包负责(mss + Pillow,Windows/Linux/macOS 通用),本插件只负责 dsh 这一侧的接线。
为什么要做原生插件,而不是直接桥接 Claude Code hook
Claude Code 的 hook 只能回传文本,能做到的极限就是把文件路径丢回去、指望 agent 自己去读。dsh 有持久图片附件服务,所以原生插件可以把截图提交进附件库、再作为真正的图片块送进对话 —— agent 什么都不用做。
| 路径 | 图片怎么到达模型 |
|---|---|
take_screenshot 工具 |
工具结果里直接带图片块 |
| 工具执行后(默认关) | 截图作为附加上下文随工具结果一起进上下文 |
| 轮次结束时(默认关) | 截图被 steer / inject 进下一步 |
前置条件
- dsh
v0.1.0-rc.8或更高 —— 本插件用到的每个 API 都是对着这个 tag 核对过的。 - PATH 上有 pnpm ——
dsh plugin是转发给它执行的。 - Python 包
screenshot-feedback-hook-mcp>= 0.3.0,可以用uvx screenshot-feedback-hook-mcp零安装运行(需要 uv),也可以pipx/uv tool常驻安装。更早的版本没有capture --json,插件会识别出来并提示你升级。 - 一个支持图片输入的模型 —— 见支持图片的模型。没有的话插件会拒绝截图,并说明怎么换。
安装
dsh plugin --profile web add dsh-screenshot-feedback-hook-mcp
dsh web
然后让 agent「截个图,告诉我屏幕上是什么」。用别的 profile 就把 --profile 换掉。
从源码 checkout 安装:
git clone https://github.com/lkh081231/screenshot-feedback-hook-mcp.git
dsh plugin --profile web add ./screenshot-feedback-hook-mcp/dsh-plugin
0.2.0 有什么变化
- 截图文件现在会留在盘上。
take_screenshot的结果里声明了path,现在这个路径指向一个真实可读的文件,你可以照着再读一次。0.2.0 之前文件在工具返回前就被删了,声明的path永远指向一个不存在的文件。插件会在临时目录里保留最近 20 张、修剪更早的;失败的截图不留任何东西。 delay_ms有上界了。 模型要一个很长的等待,不再能顶穿运维设的captureTimeoutMs。上限是 10000 毫秒,配置的delayMs更大时以它为准;被钳制时结果的 warnings 里会说明。- 图片能力闸门能区分两种拒绝了。「这个模型不声明图片输入」和「路由解析不出来」现在各记各的提醒额度,前者(告诉你该怎么换模型的那条)不会再被后者挤掉。查询模型目录时的瞬时故障两者都不算,只记日志并跳过。
- 取消和超时会分别报出来,不再都显示成「the screenshot command produced no output」。
从 0.1.0 升级(必看)
0.1.0 会把 dsh 的运行时包当成普通依赖装进 profile,在 profile 里造出第二份 @deepseek-ai/dsh-tools,盖掉 dsh 自己那份。结果不只是截图不能用 —— 该 profile 里任何工具调用都会崩:
Cannot read properties of undefined (reading 'prepare')
0.1.1 起改成 peer 依赖,不会再往 profile 里装任何 dsh 包。已经装过 0.1.0 的,把被污染的 node_modules 一并清掉再装:
dsh plugin --profile web remove dsh-screenshot-feedback-hook-mcp
rm -rf ~/.dsh/profiles/web/node_modules
dsh plugin --profile web add dsh-screenshot-feedback-hook-mcp
装完确认一下 ~/.dsh/profiles/<name>/node_modules/@deepseek-ai/ 里没有任何 dsh-*(只该有 schemastery 和 cosmokit)。
它是怎么注册进 dsh 的
本包是一个 dsh 组合包(bundle) —— 一个附带配置层的 npm 包,不需要你手写任何 patch。它靠 package.json 里的 dsh.bundle manifest 声明自己贡献什么:
{
"name": "dsh-screenshot-feedback-hook-mcp",
"main": "lib/index.js",
"files": ["lib", "cordis.patch.yml"],
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}
cordis.patch.yml 就是那一层,按包名引用插件模块(不是相对路径,否则 Node 解析不到已安装的代码):
- insert:
- id: screenshot-feedback
name: dsh-screenshot-feedback-hook-mcp
config:
command: uvx
args: ['screenshot-feedback-hook-mcp']
monitor: 0
dsh plugin --profile <name> add ... 会在 profile 目录里转发给 pnpm 装包,认出 dsh.bundle 后把包名追加进该 profile 的 dsh.profile.bundles:
{
"name": "dsh-profile-web",
"dependencies": { "dsh-screenshot-feedback-hook-mcp": "..." },
"dsh": { "profile": { "bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"dsh-screenshot-feedback-hook-mcp"
] } }
}
启动前先只验证这一层,不真的跑起来:
dsh --profile web --dump-config # 应当出现 `# == dsh-screenshot-feedback-hook-mcp` 层与 id: screenshot-feedback 那一行
卸载:dsh plugin --profile web remove dsh-screenshot-feedback-hook-mcp,依赖和对应的层会一起消失。
生效配置的层顺序是:各组合包的 patch(按 dsh.profile.bundles 顺序,@deepseek-ai/dsh-base 在最前)→ profile 自己的 cordis.patch.yml → home 级 $DSH_HOME/cordis.patch.yml → 每个 --patch overlay。所以你可以在自己 profile 的层里覆盖本包的行,不用改这个包。
配置
装好之后日常调参在设置 → 插件 → 插件配置 → 截图反馈那张卡片上,它写的是
$DSH_HOME/settings.yaml,叠在组合层之上、免重启。卡片上每个字段都标注是否被
你覆盖过,并且能一键重置回组合层的值。
组合包插入的那一行 id 是 screenshot-feedback,适合放固定的部署事实。要改它,
在自己 profile 的 $DSH_HOME/profiles/<name>/cordis.patch.yml 里写一行同 id 的
配置。后应用的层会替换整个 config,所以要重述所有你想要的键:
- id: screenshot-feedback
name: dsh-screenshot-feedback-hook-mcp
config:
command: uvx
args: ['screenshot-feedback-hook-mcp']
monitor: 1
autoAfterTools: true
autoAfterToolsDelayMs: 2000
| 字段 | 默认 | 卡片上可改 | 说明 |
|---|---|---|---|
command |
uvx |
截图可执行文件。用 pipx / uv tool 装过的,改成 screenshot-feedback-hook-mcp 并清空 args。 |
|
args |
['screenshot-feedback-hook-mcp'] |
置于子命令之前的固定参数。 | |
cwd |
'' |
子进程工作目录;留空用 host 的 cwd。 | |
monitor |
0 |
✓ | 0 = 全部显示器拼接,1..N = 单屏。编号用 list_monitors 查。 |
delayMs |
0 |
✓ | 手动截图前的等待,等页面 / 工程图渲染完成。 |
maxEdge |
1568 |
✓ | 最长边像素。不要超过 2000,附件库会拒绝更大的图。 |
targetKb |
80 |
✓ | 字节预算。dsh 没有 Claude Code 那条 25k token 的 MCP 输出上限,要看清细节可以调大。 |
captureTimeoutMs |
30000 |
✓ | 单次截图超时(在等待时间之外另算)。 |
warnOnTextOnlyModel |
true |
✓ | 闸门拒绝时提示该怎么办(纯文本模型 / 路由解析不出来)。每种原因每会话一次。 |
autoAfterTools |
false |
✓ | 命中的工具执行完就截图。 |
autoAfterToolsMatcher |
edit|write|str_replace_editor |
✓ | 工具名匹配。纯 `[A-Za-z0-9_ |
autoAfterToolsDelayMs |
1500 |
✓ | 自动截图前的等待。 |
autoOnTurnStop |
false |
✓ | 轮次即将结束时截图。 |
autoOnTurnStopDelayMs |
1500 |
✓ | 自动截图前的等待。 |
autoOnTurnStopSteer |
true |
✓ | true = steer 让模型再跑一步看图;false = 只 inject 进上下文。 |
command / args / cwd 刻意不上卡片:它们决定去哪里找可执行文件,属于部署
组合,不是用户偏好。改 config 会触发 HMR 热替换,改卡片则连热替换都不需要——
插件每次触发都重读配置。
设置页那张卡片是怎么接上去的
dsh 的插件配置标签页渲染的是两份账本的交集:Host 服务了哪些 settings 命名
空间,以及浏览器里有哪些卡片注册在这些键上。所以这个包同时提供两半,用同一个
命名空间 screenshot-feedback 配对:
- Host 半侧(
src/index.ts)用@deepseek-ai/dsh-settings的installSettingsSection注册命名空间,把cordis.yml那一行当作组合层base, 并把配置读取器指向解析后的 scope。没挂 settings 服务时它自动退回组合层, 行为与从前完全一致。 - 浏览器半侧(
src/client/)把一张 React 卡片注册进settings.plugin.item这个 keyed slot,键就是同一个命名空间。它经ctx.settingsScope读写,写入用 读取时的 revision 设栅,所以已经和文档脱节的表单会被拒绝而不是覆盖并发改动。
浏览器半侧靠 package.json 的 dsh.client 声明被发现,产物是 lib/client.js:
{
"exports": { "./client": { "default": "./lib/client.js" } },
"dsh": { "client": { "platform": "web", "inject": ["@deepseek-ai/dsh-client-ui-settings-plugins"] } }
}
[!NOTE] dsh 官方产出这种 bundle 的
clientBundle预设没有发布到 npm(官方 README 把这条列为已知限制),所以本包在 tsdown.config.ts 里自己复刻 了那份产物契约:lazy-CJS 闭包工厂、window.__ModuleLoader__.load的 banner/footer、以及只让模块表里那几个 specifier 保持require()。升级 dsh 时 要跟着复核packages/client/tsdown.client.ts与packages/client/web/src/platform.ts。
关于两个自动时机
两个默认都关:每张截图都会一直跟着后续每次请求走,直到发生压缩。
autoAfterTools挂在tools/post-execute,把截图挂到工具结果旁边。它不可能死循环,并且会跳过失败的工具调用(那时候的画面说明不了任何事)。autoOnTurnStop挂在agent/turn-stopping。在那里 steer 会强制模型再跑一步、再次回到同一个停止边界 —— dsh 的 Claude Code hook 桥接把stop_hook_active恒置为false且没有连击上限,所以照搬的 Stop hook 会让 agent 无限续跑。本插件按payload.turn去重:一个 turn 最多截一次。
截图失败绝不会阻断任何东西 —— 只记一条日志,工具流水线和轮次照常走。
支持图片的模型
dsh 只有在当前这条确切路由声明了图片输入(ctx.llm.resolveModelInfo(...).inputModalities)时才会把图片放进对话 —— 和内置 read_image 工具是同一道闸。在 dsh v0.1.0-rc.8 上,内置的 deepseek-official 路由只公布 deepseek-v4-flash 和 deepseek-v4-pro,两者都是纯文本模型。
[!IMPORTANT] 设置页声明不了模态。「设置 → 模型」的模型卡片只能编辑
id/ 名称 / 上下文窗口 / 最大输出,没有模态字段 —— 在那里新加的模型一律按纯文本处理,本插件会拒绝截图。加完自定义模型后,请点该页的**「打开配置文件」**,在settings.yaml里手工给这条模型补上input: [text, image](llm-deepseek下的字段名是inputModalities: [text, image])。手写的字段不会被之后在设置页里的编辑抹掉。
拿到支持图片的路由有三条路:
在设置 → 模型里添加 Anthropic / OpenAI 等 catalog provider,选它的视觉模型。
自定义 provider 在
$DSH_HOME/settings.yaml里声明模态:llm-pi-ai: providers: my-gateway: apiKeyEnv: GATEWAY_API_KEY api: openai-completions baseURL: https://gateway.example/v1 models: - id: vision-model input: [text, image]catalog provider 改用
modelOverrides.<模型id>.input;整条路由可以用defaultInput: [text, image]兜底。如果你的 DeepSeek 端点确实提供视觉模型,在
llm-deepseek.models里给它加inputModalities: [text, image]。
这些字段是对你端点的断言,端点本身不支持的话不会因此变得支持。
不用这个插件的其他接法
- 桥接 MCP server:
@deepseek-ai/dsh-mcp-client可以把同一个 Python 包当 MCP server 跑起来,工具名变成mcp__screenshot__take_screenshot。图片闸门一样,但没有自动截图。 - 桥接已有的 Claude Code hook:
@deepseek-ai/dsh-hooks-claude-code能跑现成的hooks.json。只用PostToolUse,matcher 里写 dsh 的小写工具名,并加--image-tool read_image,否则 agent 会被指去调一个不存在的工具。那边不要用Stophook —— dsh 上stop_hook_active恒为false,CLI 自带的防死循环逻辑根本不会触发。
开发
npm install
npm run typecheck
npm test
npm run build
真实截图的集成测试默认跳过,要跑就指向一个已安装的 CLI:
DSH_SCREENSHOT_CLI=../.venv/Scripts/screenshot-feedback-hook-mcp.exe npx vitest run
[!WARNING] 所有
@deepseek-ai/dsh-*与@deepseek-ai/cordis一律是 peer,绝不能放进dependencies。 本地开发靠devDependencies提供,运行时必须由 host 那份安装提供。把任何一个挪回dependencies,pnpm 就会在 profile 里物化出第二份副本,盖掉 dsh 建在~/.dsh/profiles/node_modules/的符号链接;而 dsh-tools 的调度器是用模块局部Symbol索引的,两份副本会让ctx.tools[TOOL_RUNTIME_SCHEDULER]变成undefined,该 profile 里所有工具调用(read/write/bash全都算)都会以Cannot read properties of undefined (reading 'prepare')崩掉。tests/packaging.spec.ts守着这条线。
MIT License.