Docs文档

故障排查

先跑 modlens doctor:它会检查你的 Node 版本、哪些 provider 已就绪(含各家有几把密钥)、将选中哪一个及其原因、冷却开关和正在冷却的密钥,以及检测到的 harness,全程不消耗额度,也不发网络请求。大多数配置问题在你继续往下读之前就能被它查出来。一把密钥用尽会先轮换到下一把,再进入冷却,下次运行会优先试还健康的密钥。

下面每条消息都是 modlens 实际会打印的。拿你看到的字眼在本文里搜索即可。

Antigravity CLI 读不到已保存的登录令牌

Antigravity CLI cannot read its stored login token.

On Linux this usually means the OS keyring is locked, which is normal for headless
sessions (agents, cron, systemd, SSH without a desktop login) ...

agy 把令牌存在操作系统钥匙串里。钥匙串被锁定时,agy 会把自己报告为未登录,并尝试浏览器登录,而没有显示器时这个流程无法完成。三条出路:

  • 解锁钥匙串,或在桌面会话里运行 modlens。
  • agy 重新登录。
  • 换一个不需要交互式登录的 provider:
modlens config set gemini-api.apiKey <key>   # free key: https://aistudio.google.com
modlens config set provider gemini-api

额度用尽

Individual quota reached. ... Resets in 94h19m9s.

agy's free tier is one weekly bucket shared by the desktop app, the CLI, and the SDK ...

等重置,或换到 gemini-api,它有自己独立的预算。并行的 subagent 会飞快耗干这个共享额度池,用得猛的一天就能把它用完。

找不到 provider CLI

Provider CLI not found: agy (spawn ENOENT). Install it and sign in first.

二进制不在 PATH 上,或者 --provider-bin 指错了地方。其他 spawn 级失败(... could not start \claude`: spawn EACCES)会保留真实错误码,方便定位。Windows 上 npm 装的 CLI 是 .cmdshim,modlens 通过 PATHEXT 解析并直接运行它背后的 Node 入口,所以裸名(ENOENT)和.cmd`(EINVAL)都不会卡住它。

Working directory does not exist: /some/path

成因不同,但操作系统返回的是同一个底层错误码:--workdir 指向了一个不存在的目录。二进制本身没问题。

recover-paste 什么都没找到

No pasted images found in any session storage for this directory (looked in: ...)

按可能性从高到低:

  • **你在错误的目录里。**恢复只限于对话所在的项目。传 --cwd /path/to/project
  • **根本没有粘贴过。**拖进来的文件和手打的路径本来就是真实文件,没有什么可恢复的:直接用那个路径。
  • **某个配置问题挡住了一个 harness。**被挡的原因会出现在同一条消息的 Blocked: 之后,例如 OpenCode 需要 Node 22.13+ 才能用 node:sqlite

recover-paste 返回了另一个项目的图片

这种情况现在不应该再出现了,真出现就是值得上报的 bug。恢复检查的是 transcript 里记录的工作目录,不只是目录名,因为目录 slug 会撞车(/tmp/a.b/tmp/a-b 生成同一个 slug)。提 issue 时带上输出里的 harnesstranscript 字段。

项目对了,图片恢复错了

输出按从旧到新排列,所以最后一条才是最近一次粘贴。harness 存了文件名时条目会带 filename:用户提到名字时按它来匹配。--count 3 能多给几个候选。

recover-paste:覆盖检测结果与输出位置

recover-paste 会自动检测自己运行在哪个 harness 里(先看进程祖先,再看环境特征),并且只读那个 harness 的存储。两个旋钮可以覆盖它:

  • MODLENS_HARNESS 不用命令行参数就能强制指定存储范围:claude-codepiopencodecodex,或 none(扫描所有存储,不限范围)。检测最先读它,所以它优先于进程祖先和环境特征。--harness 对单次运行做同样的事。
  • --out-dir 决定恢复出的图片落在哪。默认每次运行都新建一个不可预测的 <tmpdir>/modlens-paste-* 目录(0700,内含 0600 文件),没人能预先创建一个共享路径来截获字节。系统临时目录不合适时可以指到别处。显式传入的 --out-dir 若已存在,必须是真实目录(不是符号链接)、归你所有、组和其他用户无任何权限,否则会被拒绝。Windows 上会跳过所有权和权限检查,因为该平台没有 POSIX 权限位(见下方 Windows 一节)。符号链接检查仍然生效。

这是一个 Codex 会话

This is a Codex session: pasted images already exist as temp files, and each image
tag in the message carries its path.

一切符合设计。Codex 会把粘贴的图片写到磁盘,并把路径放进消息里,所以直接从 tag 里取路径来读,不需要恢复任何东西。

openai provider 的结果被拒绝

OpenAI-compatible API returned JSON that does not match the vision schema
(wrong or missing: visual.notes, ...)

那个端点返回了不符合契约的内容。注意措辞:被点名的字段可能是缺失,也可能是存在但形状不对。像 visual.notes 这样的可选字段只可能是后者,因为它缺失是被接受的。写成 null 也会被丢弃而不是拒绝,所以剩下的就是真正的类型错误。

大多数 OpenAI 兼容网关在服务端什么都不强制,契约是以填好的 JSON 模板形式随提示词发过去的,能力弱一些的模型可能只答出一半,关掉思考时尤其明显。可以改成让网关自己强制执行:

modlens config set openai.structuredOutput true

这会把契约以 response_format: json_schema 的严格形式发过去,schema 由 modlens 校验用的那份推导而来,没有需要手工同步的副本。默认关闭,因为不支持这个字段的网关会直接 400。真遇到就关回去:

modlens config set openai.structuredOutput false

你自己在 extraBody 里设的 response_format 优先级更高。

还是不行就重试一次,然后换 provider:

modlens -i <image> -p gemini-api

guard 给出了 deny,或一次读取被拒绝

Invocation guard denied this read: active model "gemini-3.1-pro" matches guards.denyModels pattern "gemini-3*". A model with native vision should read the image itself. To override, unset MODLENS_MODEL or edit guards in /Users/you/.modlens/config.json.

这是配置在按预期工作:配置文件里的 guards.denyModels 列出了自带视觉的模型,当前模型匹配到了其中一条,引擎因此拒绝为一张该模型自己就能读的图片花掉一次 provider 调用。modlens doctor 有一个 Guard 小节,展示规则、检测到的模型、来自哪个信号(MODLENS_MODEL 环境变量、会话存储或 --model 自报),以及判定结果。

如果检测错了,MODLENS_MODEL=<actual-model> modlens guard 覆盖一切,MODLENS_MODEL=none 把模型标为未知(判定随 denyWhenUnknown 走,默认 allow)。彻底关掉 guard:modlens config set guards.denyModels ''

一个已知盲区:存储检测读的是这个项目记录的最新一条 assistant 轮次,所以同一个项目目录里同时跑着不同模型的两个会话可能互相遮蔽(Claude Code 和 Codex 通过注入的会话 id 锁定确切会话,Pi 和 OpenCode 做不到)。中招时用 MODLENS_MODEL 覆盖。

注意上面那种硬拒绝只在显式的 MODLENS_MODEL 值真正匹配到 denyModels 时才触发。存储检测和 denyWhenUnknown 策略从不阻断 analyze,它们只通过 modlens guard 发声,而 guard 的 deny 是给 agent 的建议,不是上了锁的门。

dsh 提示 declares no dsh.bundle — installed as a plain dependency

dsh profile 装到的是旧版 modlens。dsh.bundle 声明从 3.9.0 起才存在,而 pnpm 11 会扣住最近 24 小时内发布的版本(minimumReleaseAge,自 11.0 起默认开启。pnpm config get 不展示这一项的内置默认值,所以查它什么都不显示)。当带声明的版本全都落在这个窗口内时,pnpm 会静默回退到更旧的版本,而旧版本没有 bundle 声明,dsh 于是正确地把它当作普通依赖,一个工具都不会出现。

@latest 绕不开这一层,本页早先的说法是错的。冷静期先把候选版本过滤掉,dist-tag 才在剩下的里面解析,于是它直接落到了更旧的那个上。改成写死精确版本号,pnpm 会把它当作一次明确的指定,而不是一次解析:

npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.24.2

npm view @liustack/modlens version 可以查到当前版本号。pnpm 11 会装上被点名的版本,11.1.3 起还会把它作为一条已批准的例外写进该 profile 的 pnpm-workspace.yaml,其余所有包和 modlens 以后的版本仍然留在窗口后面。

如果你自己设过 minimumReleaseAge,pnpm 会把这条策略视为严格模式,转而拒绝安装并报出版本与截止时间(ERR_PNPM_NO_MATURE_MATCHING_VERSION)。在同一个文件里放行这一个版本:

minimumReleaseAgeExclude:
  - '@liustack/modlens@3.24.2'

或者只为这一条命令解除冷静期,注意它解除的是这条命令解析到的所有包,不只 modlens:

npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@latest --config.minimumReleaseAge=0

dsh 的 reconcile 会注意到新版本上的 bundle 声明并激活它,随后重启 dsh。用 npx -y @deepseek-ai/dsh plugin --profile <name> list 验证。

dsh:模型看不到 read_image 工具

插件注册的工具名是 modlens_read_image,不是 read_image。dsh 的工具注册表是分层的,scoped 层会遮蔽全局层:宿主的 read_image 挂在 agent preset 作用域、插件注册在全局层,两者根本不算重名,于是注册静默成功,模型解析到的仍是宿主那个,而它对纯文本模型直接拒绝(#34)。用自己的名字就没有东西会遮蔽它,模型是通过工具 schema 找到它的,而 schema 每次请求都会送达,与叫什么名字无关。

如果模型仍然看不到,去 harness 日志里搜 [modlens] ... registration skipped。想改名就在插件配置行里设 toolName,但要挑一个别人没用的:任何已被某个 scoped 工具占用的名字,都会像 read_image 一样被遮蔽,那就又绕回本节了。

- id: modlens
  config:
    toolName: vision_read_image

fetch failed 或连接失败

Could not connect to generativelanguage.googleapis.com (UND_ERR_CONNECT_TIMEOUT). The request never reached the network. ...

API 请求根本没离开这台机器。在要靠代理才能上网的网络里这是预期表现:Node 的 fetch 默认无视代理环境变量。你明确要求走代理后 modlens 才会遵循,两种写法任选:

HTTPS_PROXY=http://127.0.0.1:7890 modlens -i shot.png -p gemini-api   # env (NO_PROXY honored too)
modlens config set proxy http://127.0.0.1:7890                        # persistent, all API providers
modlens config set openai.proxy http://127.0.0.1:7890                 # one provider only

代理只作用于 API provider 的请求。远程图片的下载路径有意保持直连并钉死 IP:它的 SSRF 防护校验的正是实际连接的那个地址,加了代理这些防护就失明了。在必须走代理的机器上,优先用本地文件,或让故障转移链把远程 URL 交给会在上游自行抓取的 provider。

配置文件问题

Cannot read /Users/you/.modlens/config.json: EACCES ... Fix the file or its permissions.

文件存在但读不了。文件缺失是正常的,所以这是真问题,不能无视。

Failed to parse ... Fix or delete the file.

JSON 无效。modlens config init --force 会写入一份干净的配置,旧内容会丢失。

超时

antigravity-cli provider timed out after 210000 ms.

--timeout 300000 重试一次。信息密集的图片在 agy 上花 15-40 秒属于正常,-m gemini-3.1-pro-high 还会更慢。无视 SIGTERM 的引擎会被升级为 SIGKILL,所以超时无论如何都会迅速返回。

推理模型上每次读取都很慢

默认思考的模型会在开始转录之前先把预算花在思考上,而视觉读取并不需要思考。没有统一的 --no-thinking 参数,因为每家厂商给这个开关起的名字都不一样,所以直接传厂商自己的字段:

modlens config set openai.extraBody '{"thinking":{"type":"disabled"}}'
modlens -i shot.png --extra-body '{"reasoning_effort":"low"}'    # one run only

各家厂商的具体写法、哪些模型完全关不掉,以及怎么确认字段真的生效,见配置手册

extraBody cannot override "messages" for the openai provider

这个字段承载着图片、prompt 和 schema 强制逻辑。把它删掉,只保留厂商的开关字段。网关返回 400 并点名你设置的某个字段,说明那个端点用的是另一种写法。在 antigravity-cliclaude-cli 上运行时,meta.warnings 会说明该值被忽略了,因为 CLI provider 没有请求体。

Windows

ModLens 可以在 Windows 上运行。三个值得了解的平台差异:

  • **没有 POSIX 权限检查。**Windows 文件没有所有者、组、其他用户的权限位(读出来是 0o666/0o777,实际访问由 ACL 控制),所以 doctor 不评判配置文件的权限模式,recover-paste --out-dir 也不会因所有权或组和其他用户的权限而拒绝目录。--out-dir 的符号链接检查仍然生效。
  • **Harness 检测依赖环境特征。**没有 ps 可以读进程树,检测只能依靠各 harness 设置的环境变量。猜错时用 --harness <name>MODLENS_HARNESS 强制指定。
  • **粘贴恢复。**OpenCode 的恢复在 Windows 上已覆盖(issue #11)。Claude Code 和 Pi 的 JSONL 路径依赖 os.homedir() 和各 harness 在那里的磁盘 slug。恢复扑空时,用 --transcript 直接指向文件,或把图片拖进终端。

还是没解决

提 issue 时附上完整命令和完整报错:https://github.com/liustack/modlens/issues