让模型自主操控隐身浏览器抓取网页并截图
31.4/100
综合分
2
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/15 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
自动兼容检查有告警:未发布到 npm registry,仅可从源码安装。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add mirelconstantin/dsh-camofox该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
dsh 原生插件 · vision
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 18 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查⚠ 装前注意未发布到 npm registry,仅可从源码安装
展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包dsh-camofox(未发布到 npm,仅可源码安装)
✓Node 引擎要求 ^22.19.0 || >=24.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/15 17:42:41
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 80 处,展示前 67 条)
- 发起网络请求src/mirror.js:370
const response = await fetch(url, { - 写入本地文件src/mirror.js:588
mkdirSync(pagesDir, { recursive: true }) - 写入本地文件src/mirror.js:589
if (includeAssets) mkdirSync(assetsDir, { recursive: true }) - 发起网络请求src/mirror.js:606
const response = await fetch(new URL('/robots.txt', origin), { headers: { 'user-agent': FALLBACK_UA }, signal }) - 写入本地文件src/mirror.js:732
writeFileSync(join(assetsDir, `${assetKey(asset.url)}${extension}`), fetched.bytes) - 写入本地文件src/mirror.js:779
mkdirSync(dirname(join(outDir, current.file)), { recursive: true }) - 写入本地文件src/mirror.js:780
writeFileSync(join(outDir, current.file), rewriteDocument(harvested.html, replacements, current.url)) - 读取本地文件src/mirror.js:814
writeFileSync(file, rewriteStylesheet(readFileSync(file), assetMap)) - 写入本地文件src/mirror.js:814
writeFileSync(file, rewriteStylesheet(readFileSync(file), assetMap)) - 读取本地文件src/addons.js:73
stats = lstatSync(path) - 读取本地文件src/addons.js:82
manifest = JSON.parse(readFileSync(join(path, 'manifest.json'), 'utf-8')) - 读取本地文件src/addons.js:212
if (existsSync(join(candidate, 'camofox.config.json'))) { - 读取本地文件src/addons.js:220
config = JSON.parse(readFileSync(configPath, 'utf-8')) - 写入本地文件src/addons.js:231
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8') - 读取本地文件src/addons.js:242
existing = readFileSync(pluginPath, 'utf-8') - 写入本地文件src/addons.js:256
mkdirSync(join(targetDir, 'plugins', name), { recursive: true }) - 写入本地文件src/addons.js:257
if (existing !== generated) writeFileSync(pluginPath, generated, 'utf-8') - 读取本地文件src/bootstrap.js:74
if (existsSync(join(base, entry))) return join(base, entry) - 读取本地文件src/bootstrap.js:95
return existsSync(join(camoufoxCacheDir(), 'version.json')) - 读取本地文件src/bootstrap.js:139
if (existsSync(existing)) { - 写入本地文件src/bootstrap.js:157
mkdirSync(repoPath, { recursive: true }) - 读取本地文件src/bootstrap.js:161
if (existsSync(afterLock)) return afterLock - 读取本地文件src/bootstrap.js:165
if (!existsSync(pkgJsonPath)) { - 写入本地文件src/bootstrap.js:167
writeFileSync(pkgJsonPath, JSON.stringify({ name: 'camofox-runtime', private: true }, null, 2) + '\n', 'utf8') - 读取本地文件src/bootstrap.js:190
if (!existsSync(installed)) { - 写入本地文件src/bootstrap.js:199
writeFileSync(rootConfig, readFileSync(engineConfig, 'utf8'), 'utf8') - 执行外部命令来自配置/变量src/bootstrap.js:290
const child = spawn(command, args, { - 写入本地文件src/bootstrap.js:338
mkdirSync(join(path, '..'), { recursive: true }) - 写入本地文件src/bootstrap.js:342
writeFileSync(path, JSON.stringify({ pid: process.pid, at: Date.now() }), { flag: 'wx' }) - 删除本地文件src/bootstrap.js:349
rmSync(path, { force: true }) - 删除本地文件src/bootstrap.js:361
rmSync(path, { force: true }) - 发起网络请求src/bridge.js:620
return await this.engine.request(method, path, body, options) - 执行外部命令来自配置/变量src/engine.js:393
spawn: (command, args, options) => spawn(command, args, options), - 写入本地文件src/engine.js:769
mkdirSync(this.config.stateDir, { recursive: true }) - 写入本地文件src/engine.js:770
writeFileSync(this.orphanPath, JSON.stringify(record, null, 2), 'utf8') - 读取本地文件src/engine.js:779
if (!existsSync(this.orphanPath)) return null - 读取本地文件src/engine.js:780
const record = JSON.parse(readFileSync(this.orphanPath, 'utf8')) - 删除本地文件src/engine.js:794
rmSync(this.orphanPath, { force: true }) - 读取本地文件src/engine.js:991
if (existsSync(configured)) return configured - 读取本地文件src/engine.js:1033
if (!existsSync(entry)) { - 写入本地文件src/engine.js:1044
mkdirSync(dirname(this.logPath), { recursive: true }) - 写入本地文件src/engine.js:1045
mkdirSync(this.config.stateDir, { recursive: true }) - 写入本地文件src/engine.js:1154
appendFileSync(this.logPath, `\n[dsh-camofox] engine exited code=${code} signal=${signal} expected=${expected}\n`, 'utf8') - 读取本地文件src/engine.js:1252
if (!existsSync(this.logPath)) return '' - 读取本地文件src/engine.js:1253
const text = readFileSync(this.logPath, 'utf8').trimEnd() - 写入本地文件src/engine.js:1372
mkdirSync(this.config.stateDir, { recursive: true }) - 删除本地文件src/engine.js:1443
rmSync(this.lockPath, { force: true }) - 发起网络请求src/engine.js:1534
return await this.request(method, path, body, options) - 发起网络请求src/engine.js:1656
return fetch(url, { - 读取本地文件src/tools.js:225
if (existsSync(target)) { - 读取本地文件src/tools.js:228
previousBytes = statSync(target).size - 写入本地文件src/tools.js:242
mkdirSync(dirname(target), { recursive: true }) - 写入本地文件src/tools.js:243
writeFileSync(target, data) - 读取本地文件src/tools.js:1640
if (!existsSync(resolved)) throw new Error(`${resolved} does not exist`) - 读取本地文件src/tools.js:1641
if (!statSync(resolved).isDirectory()) throw new Error(`${resolved} is not a folder; serve the folder that contains the file`) - 读取本地文件src/serve.js:204
const raw = readFileSync(join(stateDir, REGISTRY_FILE), 'utf-8') - 写入本地文件src/serve.js:222
mkdirSync(stateDir, { recursive: true }) - 写入本地文件src/serve.js:223
writeFileSync(join(stateDir, REGISTRY_FILE), `${JSON.stringify({ serves }, null, 2)}\n`, 'utf-8') - 读取本地文件src/util.js:278
const entries = readdirSync('/proc') - 读取本地文件src/util.js:285
const statStr = readFileSync(`/proc/${pid}/stat`, 'utf8') - 读取本地文件src/util.js:410
const statStr = readFileSync(`/proc/${pid}/stat`, 'utf8') - 读取本地文件src/util.js:462
if (!existsSync(path)) return false - 读取本地文件src/util.js:463
if (statSync(path).size < maxBytes) return false - 读取本地文件src/util.js:465
if (existsSync(oldest)) unlinkSync(oldest) - 删除本地文件src/util.js:465
if (existsSync(oldest)) unlinkSync(oldest) - 写入本地文件src/util.js:472
renameSync(source, `${path}.${index + 1}`) - 写入本地文件src/util.js:474
renameSync(path, `${path}.1`)
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
Camoufox 原生接入 DeepSeek Harness:受管引擎、十六个原生浏览器工具、站点镜像,以及人类看到的截图卡片。
README
由 DeepSeek 最新模型翻译生成dsh-camofox
Camoufox 作为 DeepSeek Harness 的一等能力:受监管的浏览器引擎、十六个原生工具、一段系统提示词,让每个模型都知道浏览器就在那里、什么时候该用它,还有一张人类能直接看到的截图卡片。
致谢 —— 它驱动的浏览器
Camoufox —— <https://github.com/jo-inc/camofox-browser> —— Copyright (c) 2025 Jo, Inc, MIT。
Camofox 是这个插件围绕构建的隐身浏览器:它提供引擎、指纹面,以及这里每个工具都会配置的反爬绕过能力。本包不再分发它。 插件首次运行时自行安装引擎,来源是 @askjo/camofox-browser,也就是 camofox-browser 项目在 npm 上的发布版本。NOTICE 里写了这段署名。见 LICENSE。
引擎是拉取的,不是打包进来的:它包含约 175 个 npm 包,浏览器二进制约 1.06 GB,每台机器缓存一次,放在安装目录之外。
安装
dsh plugin add dsh-camofox --profile desktop
安装就这一步。第一次调用浏览器工具时会把引擎装到 ${DSH_HOME}/camofox/repo —— 有线网络下大约一分半,大部分时间花在网络上 —— 之后每次调用都复用它。
展开完整文档(README 剩余 1 节)
dsh-camofox
Camoufox 作为 DeepSeek Harness 的一等能力:受监管的浏览器引擎、十六个原生工具、一段系统提示词,让每个模型都知道浏览器就在那里、什么时候该用它,还有一张人类能直接看到的截图卡片。
致谢 —— 它驱动的浏览器
Camoufox —— <https://github.com/jo-inc/camofox-browser> —— Copyright (c) 2025 Jo, Inc, MIT。
Camofox 是这个插件围绕构建的隐身浏览器:它提供引擎、指纹面,以及这里每个工具都会配置的反爬绕过能力。本包不再分发它。 插件首次运行时自行安装引擎,来源是 @askjo/camofox-browser,也就是 camofox-browser 项目在 npm 上的发布版本。NOTICE 里写了这段署名。见 LICENSE。
引擎是拉取的,不是打包进来的:它包含约 175 个 npm 包,浏览器二进制约 1.06 GB,每台机器缓存一次,放在安装目录之外。
安装
dsh plugin add dsh-camofox --profile desktop
安装就这一步。第一次调用浏览器工具时会把引擎装到 ${DSH_HOME}/camofox/repo —— 有线网络下大约一分半,大部分时间花在网络上 —— 之后每次调用都复用它。别的什么都不需要:不用 clone,不用再跑一次 npm install,不用 repoPath。
引擎需要 Node 22 或更高版本、能访问 npm registry 的网络,以及首次运行约 1.2 GB 的空闲磁盘。它的浏览器二进制(约 1.06 GB)每台机器缓存一次,放在安装目录之外,所以同一台机器上的第二个 harness 不用再付这份代价。
自带引擎,或者离线安装
插件只会装到默认位置,所以你设的 repoPath 是你自己管理的路径,插件永远不会往里写东西。把它指向已有的 camofox-browser 检出目录 —— 一个 git clone,或者某次 npm 安装留下的 node_modules/@askjo/camofox-browser 目录 —— 就会直接拿来用:
- insert:
- id: camofox-runtime
name: './src/index.js'
config:
repoPath: '/absolute/path/to/camofox-browser'
想彻底关掉自动安装,把 bootstrap 设为 false。一台没有注册表访问随后会报告引擎预期所在的位置,而不是尝试去访问它。
其他方式
| 方式 | 命令 |
| 固定到某个版本 | dsh plugin add dsh-camofox@1.1.1 --profile desktop |
| 固定到某个提交 | dsh plugin add github:mirelconstantin/dsh-camofox#<sha> --profile desktop |
| Tarball | dsh plugin add ./dsh-camofox-1.1.1.tgz --profile desktop |
| 本地检出 | dsh plugin add /absolute/path/to/dsh-camofox --profile desktop |
它替代了什么
它替代了早先的 mcp__camofox__ stdio 桥接。那个桥接对外宣称有 26 个工具,其中四个根本用不了,而且 harness 退出后它还会让浏览器引擎继续运行。
如果某个工具调用仍然返回 MCP resource server "camofox" is unavailable in this agent's scope,那是在调用这个包已经移除的桥接,不是这里的错误:并不存在名为 camofox 的 MCP 服务器。请改用原生的 camofox_ 工具。
你能得到什么
| 工具 | 用途 |
| camofox_open | 打开页面(或搜索宏),拿到 tab id,可选等待选择器,让延迟渲染的页面就绪。会报告 navigationOk 和 httpStatus。传 trace:true 可录制 Playwright trace,记录浏览器做的一切 |
| camofox_read | 读取页面:snapshot(无障碍引用)、text、markdown、links、images、html、stats。快照可以用 interactive:true(只保留可操作节点)或 scope(单个子树)缩小范围 |
| camofox_act | click · type · select · press · scroll · wait · waitForSelector · waitForUrl · upload · viewport · back/forward/refresh |
| camofox_capture | 截图并附到对话里,或生成 PDF 写入 savePath。fullPage:true 会截取整个可滚动页面 |
| camofox_eval | 在页面里运行 JavaScript,处理读取工具表达不了的东西 |
| camofox_search | 一次调用:搜索 Google、YouTube、Amazon、Reddit、Wikipedia、X、Yelp、Spotify、Netflix、LinkedIn、Instagram、TikTok 或 Twitch,直接返回已经读好的结果 |
| camofox_scrape | 用 CSS 选择器从多个页面各提取一条记录,返回换行分隔的 JSON,或写入 outputPath |
| camofox_mirror | 把网站复制到一个可离线使用的文件夹:页面及其 CSS、JS、字体和图片,链接会重写 |
| camofox_tabs | 列出、关闭、全部关闭、重置会话,或让引擎释放内存 |
| camofox_status | 引擎健康状态、内存、所有会话中打开的 tab,以及引擎日志路径 —— 浏览器工具出问题时先用它 |
| camofox_transcript | YouTube 视频的口述字幕 |
| camofox_cookies | 导入 cookies,让页面打开时就已经登录 |
| camofox_console | 本会话页面记录的错误 —— 控制台错误、未捕获异常、失败请求、HTTP 错误 |
| camofox_fingerprint | 浏览器如何自我描述,以及哪些读数不一致 |
| camofox_trace | 列出、下载或删除 camofox_open(trace:true) 记录的 trace 归档 |
| camofox_serve | 通过 http 为浏览器提供本地文件夹,并设有硬性生命周期上限 |
抓取多个页面靠选择器,不靠 ref。 引擎自带的提取器由无障碍 ref 驱动,而 ref 只对已经读过的页面存在——对你还没见过的页面就毫无用处。因此 camofox_scrape 用 CSS 选择器填充 schema,在每个页面里求值,可选配 attr(读取属性而非文本)、all(收集所有匹配项)和 type(string / number / boolean)。当没有任何属性带 selector 时,仍会自动选择 ref 模式,所以一串 x-ref 提示组成的 schema 依然能用。
为什么是这种形态
原生工具,不是 MCP 服务器。 MCP 桥接在模型和浏览器之间塞了一个 stdio 进程,每项能力都继承了它的限制:harness 给 MCP 调用传了取消信号和超时,桥接把两者都丢了;引擎启动失败时,failOnStartupError: false 会悄悄注销所有工具;MCP 客户端在 dispose 时只杀直接子进程,所以它启动的引擎在 harness 结束后还活着。原生工具拿到的取消和截止时间与 harness 其余部分一致,失败时会大声报错,并且归一个插件所有,其 disposer 保证会执行。
十六个工具,不是二十六个。 旧命名空间重叠严重(history 对 navigate,evaluate 对 extract,screenshot 对快照内嵌的截图),逼着模型读大段文字才能挑一个。这里的每个工具只干一件事,描述里写明什么时候不要用它,schema 让参数一目了然。
不会说谎的描述。 旧工具里有四个坏在自信的描述背后:upload 发的是 files,而引擎读的是 path,所以永远只能返回 HTTP 400;wait 宣传了一个引擎从未实现的 CSS 选择器;traces 永远返回不了任何东西,因为没有工具开启过 tracing,而 trace 只在会话关闭时才刷盘;save_pdf 承诺文件会落到工作区,可那些字节通过任何路径都拿不到。这四个在这里被修复、重新划定范围或替换。traces 现在是 camofox_trace,而 camofox_open(trace:true) 才是让它返回东西的关键——已对着真实引擎验证过,它写出了一个 3.4 MB 的归档,工具随后列出、下载并删除了它。
引擎自己的词汇。 引擎把出错原因编码在响应体的 code 字段里(lib/browser-errors.js),光看状态码分不清它的各种情况:页面崩溃、标签页被销毁和真正的浏览器重启都是 410。只看状态码做判断,会告诉模型页面崩溃是浏览器重启,还让它继续。现在每个引擎 code 都保留自己的名字、自己的指令,以及引擎自己的 retryable 标志,所以改动页面的操作,绝不会在引擎标记为不可重复的条件下重放。
一切都有边界。 每个工具都声明自己的调用截止时间,所以批量任务不会无界运行。结果先整体渲染,之后再裁剪,因此调用方继续读取所需的分页字段永远不会是被截断的部分,游标行也打印在正文之前,这样后续裁剪不会删掉继续读取的指令。快照按引擎自己的窗口分页,截图按需开启,转录分页,抓取结果只要给出 outputPath 就写入文件——不给就内联返回,并按工具自己的 20 000 字符结果预算裁剪。
一页承载的内容,绝不会超过模型实际收到的量。 引擎自己把长文档切进一个最多约 79 850 字符的窗口,并带上自己的游标,而工具的结果预算比这更小。所以 camofox_read 会按一个构造上就精确的游标再细分窗口——包括引擎的最后一个窗口,它最容易溢出,因为一次从短文档开头附近继续的读取,可能拿到一个 60 000 字符的末尾窗口。
切分点不是 maxChars,也不是预留的常量:页面会按调用方自己的渲染字段计价,然后选出仍能装进工具自身内容预算的最长一页。这一点很关键,因为预算花在美化打印的 JSON 上,页面里一个换行或引号要占两个字符——预留额度在纯文本上看着没问题,却会悄悄丢掉每个真实页面的尾部。在窗口化读取中,hasMore 是引擎给出的答案,或者是一句声明:这个窗口仍持有后续窗口不会再提供的内容。
一套诚实的失败分类。 引擎已经编码了出错原因。the tab no longer exists 意味着开一个新标签页;element refs are stale 意味着重新读取页面;the camoufox browser restarted 意味着继续。这三者以指令而非状态码的形式到达模型,每个失败在向外传递时都保留自己的 code,所以会话日志仍能区分 tab_not_found 和 engine_timeout。
同一件事不做两遍。 改变页面的操作——click、type、press、scroll、select、upload——只发送一次,这些调用的重试预算为零,所以即使失败证明引擎从未收到,也不会重放。读取最多重试两次,少数声明为无害的 POST 也一样(开标签页、导航、等待、调整视口大小、求值、清理);它们只在引擎自己归类为派发前失败的错误上重复(concurrency_timeout、browser_launch_timeout)。引擎不可达不算这种证明——响应丢失和连接被拒,传输层都会抛出它——所以结账表单不会被提交两次。
生命周期
引擎是 harness 的子进程,不是游离的守护进程:
- 随 harness 启动。 进程在插件激活时派生。激活不会等待它——加载器会等待插件主体,任何注入 camofox 的东西都会保持挂起,直到该主体 resolve——所以是第一次工具调用等待就绪,而不是整个 harness 等待浏览器启动。
- 随 harness 停止。 正常退出时保证触发 Cordis 清理(上限五秒),并杀掉整个进程树,所以不会有 Camoufox 或 Firefox 子进程成为孤儿。同步的 process.on('exit') 守卫覆盖清理无法触及的路径。
- 既熬不过陈旧锁,也熬不过陌生进程。 某个引擎的 pid 出现在本 bundle 的锁文件里,且记录的属主进程已消失,那它就是早先运行留下的孤儿:会被杀掉并替换,所以浏览器确实随 harness 停止。其他任何情况都会被收养,我们绝不杀掉——包括属于仍在运行的 harness 进程的引擎,这正是锁要记录属主 pid 的原因。熬过杀死的进程会保留其锁文件,因为丢掉它会让下一次运行收养孤儿,而不是收割它。「其他任何情况」由身份检查收窄,而不只看可达性:只有当引擎自报身份(engine: "camoufox")时,健康响应才被采信,所以占用端口的无关服务会被上报,而不是被收养并拿到浏览器 API。
- 新任务收养;新进程替换。 两者都会验证。运行中进程里的第二个 supervisor 发现引擎已在应答端口,就收养它——owned 保持 false,不派生子进程,其标签页和 cookie 仍在。全新进程里的 supervisor 发现锁指向已死属主,就停掉那个引擎并启动自己的。收养带走的是浏览器,不是会话:上下文按会话划分,所以新任务仍是登出状态。
- 会恢复,放弃时也会说明。 意外退出会以带抖动的指数退避重启,受滚动窗口内的 restartLimit 约束——所以一个反复短暂触达 /health 的崩溃循环会触发熔断,而不是永远自报「第 1 次尝试,共 3 次」。完全无法启动的子进程会在大约一秒内失败,并给出 OS 错误和可执行文件,而不是耗尽整个就绪预算。
- 在回环上做认证。 本插件启动的引擎要求 bearer token,该 token 按 harness 进程生成;插件在每个请求上都发送它。只是被收养的引擎绝不会收到它不可能知道的 token,而一次无凭据重试让收养继续可用。
- 日志轮转。 engine.log 上限 5 MiB,保留三代,由 supervisor 轮转,所以引擎从不会看到轮转发生。
配置
本包是 bundle,所以它贡献一行 Loader 行,camofox-runtime,它拥有引擎,注册工具并发布提示词片段。配置写在 cordis.patch.yml 里对应的那一行,配置档级别的补丁可以按 id 覆盖它。
那一行必须保持唯一。这个包还声明了 dsh.client 这一半,而 @deepseek-ai/dsh-client-modules 会按包名在 Host Loader 的活动行之间核对它找到的来源——多出一行就会让它抛错,而这个抛错会把整个 web shell 拖垮,而不是降级运行。发布的包里会断言行数,所以不小心多写一行会触发检查失败,而不是让 GUI 出问题。
值得了解的配置项:
| 键 | 默认值 | 含义 |
| repoPath | ${DSH_HOME}/camofox/repo | 引擎所在位置。默认值在首次运行时创建;你自己设置的路径会原样使用,永远不会被写入 |
| bootstrap | true | 首次运行时,如果默认位置为空就安装引擎。对你设置的 repoPath 永远不生效。设为 false 则要求引擎已存在 |
| bootstrapTimeoutMs | 1800000 | 首次运行安装的时间预算,要下载约 175 个包和一个约 1.06 GB 的浏览器二进制文件。与 startTimeoutMs 分开,因为这是另一种等待 |
| nodePath | (空)→ process.execPath | 给引擎用的真实 Node 可执行文件。留空表示「用运行 harness 的那个」,也就是 Electron 二进制文件——插件会用 ELECTRON_RUN_AS_NODE 运行它,所以通常没问题。如果引擎起不来,把它设成一个普通的 node |
| baseUrl | http://127.0.0.1:9377 | REST 引擎 |
| autostart | true | 没有响应时启动引擎 |
| startTimeoutMs | 180000 | 启动后的就绪时间预算 |
| requestTimeoutMs | 120000 | 单个 REST 请求的每次调用截止时间 |
| shutdownTimeoutMs | 2000 | 树杀确认等待时间,在宿主五秒的销毁上限之内 |
| restartLimit | 3 | 滚动 60 秒窗口内允许的重启次数——就绪探测无法重置它,因为崩溃循环会不断访问 /health |
| reuseTimeoutMs | 15000 | 等待一个已存在的引擎进程绑定端口的时间,超时后就当作卡死并替换它。永远不超过 startTimeoutMs:一个活着但没在监听的进程就是卡住的引擎,在它身上等满整个启动预算只会拖延恢复 |
| userId | dsh | 浏览器身份的命名空间。实际发送给引擎的 userId 是 ${userId}~<sanitised session id>-<digest> |
| perSessionScope | true | 设为 false 会恢复成每个配置档共用一个 cookie jar 和标签页池 |
| scopeIdleMs | 900000 | 会话最后一次调用之后,其浏览器上下文保留多久 |
| accessKey | 每个进程生成 | 引擎要求的 Bearer token。为空时生成,并作为 CAMOFOX_ACCESS_KEY 导出给子进程 |
| allowUnauthenticated | false | 设为 true 会完全不带密钥启动引擎——只适用于你自己运行的引擎 |
| allowEvaluate | true | 设为 false 可拒绝 camofox_eval |
| defaultMaxChars | 20000 | 默认结果预算 |
| maxTabs | 8 | 一个会话保留的标签页数量,超出后关闭最久未使用的那个 |
| maxConcurrentPerUser | 3 | 一个会话可以同时进行的调用数。故意设了上限:引擎自身对每个用户有独立限制(默认三个),所以这里是兜底,防止泄漏卡住浏览器 |
| scrapeConcurrency | 3 | camofox_scrape 一次抓取的页面数,也是 camofox_mirror 每页并行下载的资源数 |
| maxExtractChars | 60000 | 只在一处生效:作为 camofox_transcript 的默认字符预算。camofox_scrape 从不读取它——那个工具受自身 500-URL 上限、maxItems 和 20000 字符结果预算约束,所以没有 outputPath 时,它会把抓到的每条记录都内联返回 |
| addons | [] | 要加载的 已解压 Camoufox 插件文件夹绝对路径。见 浏览器插件 |
| disableDefaultAddons | false | 设为 true 会去掉 uBlock Origin,Camoufox 默认会加载它。引擎不支持按名称排除,所以没有名称列表键 |
| captureConsole | true | 安装 camofox_console 背后的引擎插件。见 排查页面为什么坏了 |
| consoleMaxEntries | 200 | 一个标签页保留多少条记录,超出后丢弃最旧的。每个来源也有上限,所以错误循环无法挤掉解释它的异常 |
| stateDir | ${DSH_HOME}/camofox | 引擎锁、日志和文件服务器 serves.json 注册表所在位置。改动它也会改变 camofox_serve 记录启动内容的位置 |
| mirrorAssetLimit | 150 | 一次 camofox_mirror 运行下载的资源数,跨所有页面 |
| healthIntervalMs | 20000 | 多久重新探测一次运行中的引擎,以及日志何时轮转 |
按会话隔离
浏览器属于 DSH 会话,不属于 harness 进程。每次调用时的引擎 userId 是 ${userId}~<sanitised session id>-<digest> ,引擎会把每个都变成自己的 Playwright 上下文,所以:
- 每个会话——以及你委派的每个子代理——都有自己的 cookie jar;
- 一个会话的 camofox_tabs resetSession 无法清掉另一个会话的登录状态;
- 标签页只能由打开它的会话关闭。
digest 不是装饰。可读前缀经过 sanitise,而 sanitise 是有损的——sub/agent-01 和 sub-agent-01 都会变成 sub-agent-01——所以没有 digest 的话,两个不同会话会悄悄共用一个 jar。前缀让作用域在日志行里可读;digest 才是把两个会话分开的东西。digest 是 32 位、非加密的:它区分的是 sanitise 冲突,也就是这里真正要紧的失败,并不声称全局唯一。
引擎自身的 sessionKey 不是 cookie 边界:它是一个标签页组同一个上下文内。它仍然给标签页加标签,方便整理,而且现在默认用会话作用域,这样两个会话永远不会在组名上撞车。
Cookie 不会跨 DSH 会话保留。 这是有意为之,也符合平台自己的浏览器不变式——「浏览器配置档和登录状态不会从会话日志恢复」。把 perSessionScope 设为 false,就能回到以前「一个配置档一个 jar」的行为;这只是一行配置档改动,不需要改代码。
会话被销毁时,插件自己关闭它的上下文,而不是等引擎的回收器。scopeIdleMs 是兜底,应对框架从未宣告过的会话。
一个诚实的限制。 插件只能拒绝它自己知道属于另一个会话的标签页 id。插件早先一次运行留下的标签页——harness 重启了,引擎还活着——不在任何注册表里,所以无法归属,会被放行。引擎自己的 userId 作用域在那里仍是真正的权威,这也是为什么本地拒绝只是一个快速、易读的错误,而不是安全边界。
看清页面为什么坏了
页面级故障对其他所有工具都是隐形的。未捕获的异常、被拒绝的 promise、失败的 API 调用,都会让无障碍树和提取出的文本看起来完全健康,于是读页面的智能体没有任何可反应的东西——它只看到一个空面板,不知道原因。
camofox_console 补上了这个缺口。引擎插件在每个标签页创建时给它挂上 Playwright 监听器,并为每个标签页保留一个有上限的缓冲区:
| 捕获 | 不捕获 | 原因 |
| console.error、console.warning | console.log、console.info | 每次渲染都打日志的页面会把真正重要的两条埋掉 |
| 未捕获的异常 | — | 它们根本不会到 console,而且通常才是真正的原因 |
| 失败的请求 | 页面自己取消的请求 | ERR_ABORTED 是页面改了主意,不是失败 |
| HTTP 响应 ≥ 400 | 2xx 和 3xx | 调用成功了 |
解析不了的主机不会到达失败请求监听器:浏览器在请求离开本机之前就让它失败了,页面看到的是 NetworkError,它归在未捕获异常下面。所以 DNS 失败会被报告,只是归在另一个标题下。
条目只存在内存里,而且只针对本会话打开过的标签页。缓冲区在总量和每个来源上都设了上限,因为处于渲染循环的页面产生错误的速度可能快过任何人阅读的速度,而长期存活的引擎进程里无上限的缓冲区就是内存泄漏。
把 captureConsole 设为 false 可以关掉插件。
提供本地文件(camofox_serve)
引擎屏蔽除 http 和 https 之外的所有 URL scheme(camofox-browser/server.js:190,ALLOWED_URL_SCHEMES),这个屏蔽是上游测试过的安全决策——file:// URL 会读取 harness 能读的任何文件。所以本地复现必须先被提供出来,浏览器才能打开 it. camofox_serve 就是干这个的,难的不是把服务跑起来,而是确保服务器不会比需要它的工作活得更久。
{ "action": "start", "root": "C:\\Users\\me\\project\\.tmp-research" }
它会挑一个空闲的 loopback 端口,在 / 提供 index.html,然后返回 URL,交给 camofox_open 使用。它只绑定 loopback,并把每条路径都限制在服务的根目录内——遇到 .. 或符号链接会返回 403,而不是去读磁盘。
靠三套独立机制,什么都不留下
替智能体启动的服务器,是没人要求过的资源;一旦它比智能体活得久,就变得不可见:没有窗口,没有输出,只占着一个端口。下面每套机制都可能单独失效,所以这个保证不依赖其中任何一个:
| 层级 | 覆盖范围 | 何时失效 |
| 子进程自己的截止时间 | 它上面没有任何东西需要活着 | 不会失效,除非有针对子进程本身的 SIGKILL |
| 独立的看门狗进程 | harness、插件或工具调用挂掉 | 不会失效——它只听时钟的 |
| 插件销毁与启动清扫 | 常规路径,以及崩溃留下的任何东西 | 只有当两个进程都在 TTL 内被杀掉时 |
看门狗是独立的分离进程,每两秒重新读一次注册表,所以它也能覆盖在它启动之后才启动的服务器。每次启动都记录在插件状态目录下的 serves.json 里,正因如此,之后的运行、camofox_serve action:"stop",或插件自己的启动流程,都能找到并杀掉它们,而不用靠进程列表去猜。
实测,不是嘴上说说。 一个设了 3 秒上限的服务器,之后完全不管它,在 3049 ms 时停掉。第二个服务器在看门狗被杀掉、且进程内定时器被禁用之后启动,在 3066 ms 时停掉——所以截止时间不依赖看门狗。
上限是个常量,SERVE_TTL_MS = 120000。它不能按调用配置:调用方能抬高的保证,就不算保证。
它不做的事: 它不会让 file:// 能用,也不是运行项目的替代品。如果应用有 dev server,就用那个——页面会加载真实的资源、模块和 API 调用,这是静态文件服务器无法复现的。camofox_serve 是给自包含的 HTML 复现用的,背后不需要任何应用。
浏览器附加组件
Camoufox 加载的是解压后的附加组件文件夹——不是 .xpi 压缩包。要用 .xpi,把它重命名为 .zip,解压,然后让 addons 指向解压出来的文件夹。每个文件夹都必须包含 manifest.json,并且必须声明 manifest_version: 3:自带的 Firefox 152 禁用了 Manifest V2,所以 MV2 附加组件启动时不会报错,只是永远不运行。
- id: camofox-runtime
config:
addons:
- 'C:\Users\mirel\.dsh\camofox\addons\my-addon'
disableDefaultAddons: true 会移除 Camoufox 默认安装的 uBlock Origin 副本,当某个网站需要看到广告请求时,就该用这个设置。
没有按名称排除的选项:引擎在启动时会把那个布尔值变成硬编码的 exclude_addons: ['UBO'],所以配置里接受一串名称,实际却什么都不做。注意,去掉 UBO 也会去掉编译进 Camoufox 构建里的自定义隐私过滤器。
插件如何到达浏览器
camoufox-js 的 launchOptions() 会消费 addons 参数,并把整个生成的配置编码进解析后的 Playwright 选项上的 CAMOU_CONFIG_<n> 环境变量分块。引擎在这一切发生之后才发出会修改状态的 browser:launching 钩子,所以在那里给 options.addons 赋值没有用——浏览器照常启动,插件悄无声息地缺席。
因此本插件会安装自己的引擎侧插件 plugins/dsh-addons/index.js,它在该钩子上拼接已有的 CAMOU_CONFIG_<n> 分块,把配置的文件夹加入 config.addons,再用 camoufox-js 自己的 2047 字符 Windows 上限重新分块。插件列表通过 CAMOFOX_DSH_ADDONS 传递,以 NUL 分隔,因为 Windows 路径可能包含任何原本需要转义的字符。
引擎从 <repoPath>/plugins/<name>/index.js 加载插件,其 camofox.config.json 里的 plugins 键是白名单:文件没有点名的目录会被跳过。两者都位于引擎检出目录内,而不是本包内,这就是 installAddonPlugin 在启动时写入它们的原因。只有当该文件已经有对象形式的 plugins 键、且其中没有这一项时,它才会添加白名单条目——没有 plugins 键的文件会加载每个目录,添加一个反而会收窄范围。
所以本插件会写入你的 camofox 检出目录:<repoPath>/plugins/dsh-console/ 用于页面控制台捕获,以及 <repoPath>/camofox.config.json 里可能的一条白名单条目。插件卸载时两者都会撤销。设置 captureConsole: false 可保持检出目录不被改动。
文件夹缺失,或没有 manifest.json,会被跳过并记一行日志,而不是让启动失败。引擎上的 GET /dsh-addons/status 会报告每个配置的文件夹及其 exists 和 manifest 判定结果。
一个限制。 该集成仅限运维人员使用:插件来自 cordis.yml 配置,没有任何工具能让模型加载或更改插件。模型若能往浏览器里写入插件,就等于在页面上下文里给自己任意代码执行能力,这比浏览大得多。
截图卡片
camofox_capture 返回 PNG 作为持久附件。本包附带客户端那一半,把它在对话中内联渲染成点击放大的缩略图,而不是在图片该在的地方留一个 JSON blob。
它只负责呈现。它不注册任何工具,不碰任何工具结果,也接触不到模型看到的任何东西——它绘制的每一个字节都在浏览器能观察到之前就已由宿主侧提交。它声称的线上工具名是 camofox_capture,与宿主注册的同一个,而卡片直接替换该键对应的通用行,因此每种情况都能渲染:捕获失败、捕获没有产生任何字节,以及调用仍在运行。
它刻意保持两个特性:
- The render is total. A card that throws does not degrade — the renderer abdicates that slot key permanently, so every later capture would render as an empty box. A result part that cannot be serialized (a circular structure, a BigInt) is described instead of formatted, and an image that never loads falls back to the text the generic card would have shown.
- The copy is locale-owned. Every string comes from a dictionary through the t the renderer
builds for this registration, in English and Romanian.
This half is served at /plugins/dsh-camofox/client.js and runs as a classic script. It is part
of this package rather than a second one, and that is deliberate: @deepseek-ai/dsh-client-modules
reconciles the sources it finds by package name across the Host Loader's live rows, so a package
carrying a client half has to resolve from exactly one row. Two rows here would throw at activation
and leave the web shell with no module loader at all.
Notes and limits
- The engine caps concurrency at three calls per user, as does this plugin, so
the browser is a scarce resource on purpose: read a page, act, read again
beats firing a dozen calls at once.
- camofox_mirror copies what a browser can see: rendered HTML and the assets
it references. It cannot recover the pre-hydration document of a single-page
app, and JavaScript that fetches from an absolute CDN origin at runtime will
still reach the network.
- camofox_transcript is fast when yt-dlp is installed and falls back to
reading captions in the browser otherwise.
- Page content is untrusted. The prompt section tells the model so explicitly.
- Writes are unrestricted by design. savePath, outputPath and
camofox_mirror's outDir may name any path, and the plugin runs in the host
process, so a model mistake can write anywhere. What it will no longer do is
hide the consequence: every write reports replacedExisting, and
overwrite: false turns a collision into a refusal instead of a lost file.
This is a deliberate decision, not an oversight — do not "fix" it by adding a sandbox without changing the decision first.
- allowEvaluate gates model-authored JavaScript only. It removes
camofox_eval; it does not stop the fixed scripts compiled into this bundle — camofox_read's text, markdown and html modes, camofox_scrape in selector mode, and the waitForSelector wait on camofox_open, camofox_act and camofox_mirror still run. Gating those too would remove most of the product, and the model never supplies that code.
- camofox_tabs(action:'cleanup') asks the engine to release idle memory across
every session sharing the engine; the other camofox_tabs actions affect
the calling session only.
- file:// is blocked by the engine and stays blocked. This plugin does not
work around it; camofox_serve is the supported route for a local file, and it
exists precisely because a workaround that reads arbitrary paths is not worth
having.
- requestfailed does not cover a name that does not resolve. The browser
fails that before the request leaves the machine, so the page sees a
NetworkError and it arrives under uncaught exceptions instead. The failure is
reported, under a different heading.
- Console capture attaches per page, from three hooks. tab:created fires
after the first navigation resolves (server.js:3043), so a listener attached
only there would miss every error a page raises while loading. The capture
therefore also subscribes to context.on('page') at session creation, which
is the only place a page can be reached before its first load.
- Known engine limitations that cannot be fixed from here. Coordinate
clicking is not available: the handler destructures only {userId, ref,
selector} and answers 400 without them, so exposing x/y on camofox_act
would produce an error. main_world evaluation does not exist in the engine,
so camofox_eval is page-world only. Neither is a choice this plugin makes.
- Decisions deliberately not taken. Storage-state export is refused because
it contradicts the session-isolation invariant below; a persistent fingerprint
is refused because a frozen identity is the wrong default for an agent; and
POST /stop is refused because this plugin's supervisor owns process death.
Each of these is available upstream and each was left alone on purpose.
dsh-camofox — Camoufox as a native DeepSeek Harness capability. MIT. Camoufox is Copyright (c) 2025 Jo, Inc, MIT, and is not redistributed here. Built by Pazalo.预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
mirelconstantin/dsh-camofox
让模型自主操控隐身浏览器抓取网页并截图
相关插件 · 视觉能力
智能体工作台Devin-AXIS/iPolloWork
企业级、本地优先的智能体工作台,面向个人和智能体团队——一个工作区,容纳多个智能体引擎;一套统一体系,管理插件和技能;支…
★ 7,332已汉化DSH
72
开源设计工作台nexu-io/open-design
OpenDesign:首个协作式 AI 设计智能体工作区
★ 105,153已汉化DSH
71.8
多智能体编码工作区spacering-net/codeg
多智能体协作的 AI 编程工作区:汇总 Claude Code、Codex、OpenCode、Pi、Grok Build 等来源的会话。支持桌面应用、自托管服务器或 Docker。
★ 4,244已汉化DSH
71.6
GPT 图像提示词库freestylefly/awesome-gpt-image-2
Prompt as Code | GPT Image 2 / 2.5 提示词与案例库,530+ 个案例、20+…
★ 34,534已汉化DSH
71.5
架构图生成器tt-a1i/archify
把你想理解、规划或分享的事,变成可以互动的可视化作品。
★ 92,046已汉化DSH
71.1
AI 用量桌面小部件Javis603/token-monitor
EN | 简 | 繁 | KO | JA | PT
★ 3,480已汉化DSH
71