--- summary: '故障排查:modlens 可能打印的每一条报错、成因与解法' read_when: - 运行失败了,报错信息看不明白 - recover-paste 什么都没找到,或找到了错的图片 - 判断一次失败属于配置问题、额度问题还是 bug --- # 故障排查 [English](troubleshooting.md) | 中文 先跑 `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: ```bash modlens config set gemini-api.apiKey # 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 是 `.cmd` shim,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 时带上输出里的 `harness` 和 `transcript` 字段。 ## 项目对了,图片恢复错了 输出按从旧到新排列,所以**最后**一条才是最近一次粘贴。harness 存了文件名时条目会带 `filename`:用户提到名字时按它来匹配。`--count 3` 能多给几个候选。 ## recover-paste:覆盖检测结果与输出位置 `recover-paste` 会自动检测自己运行在哪个 harness 里(先看进程祖先,再看环境特征),并且只读那个 harness 的存储。两个旋钮可以覆盖它: - **`MODLENS_HARNESS`** 不用命令行参数就能强制指定存储范围:`claude-code`、`pi`、`opencode`、`codex`,或 `none`(扫描所有存储,不限范围)。检测最先读它,所以它优先于进程祖先和环境特征。`--harness` 对单次运行做同样的事。 - **`--out-dir`** 决定恢复出的图片落在哪。默认每次运行都新建一个不可预测的 `/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 模板形式随提示词发过去的,能力弱一些的模型可能只答出一半,关掉思考时尤其明显。可以改成让网关自己强制执行: ```bash modlens config set openai.structuredOutput true ``` 这会把契约以 `response_format: json_schema` 的严格形式发过去,schema 由 modlens 校验用的那份推导而来,没有需要手工同步的副本。默认关闭,因为不支持这个字段的网关会直接 400。真遇到就关回去: ```bash modlens config set openai.structuredOutput false ``` 你自己在 `extraBody` 里设的 `response_format` 优先级更高。 还是不行就重试一次,然后换 provider: ```bash modlens -i -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= 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 会把它当作一次明确的指定,而不是一次解析: ```sh npx -y @deepseek-ai/dsh plugin --profile add @liustack/modlens@3.17.2 ``` `npm view @liustack/modlens version` 可以查到当前版本号。pnpm 11 会装上被点名的版本,11.1.3 起还会把它作为一条已批准的例外写进该 profile 的 `pnpm-workspace.yaml`,其余所有包和 modlens 以后的版本仍然留在窗口后面。 如果你自己设过 `minimumReleaseAge`,pnpm 会把这条策略视为严格模式,转而拒绝安装并报出版本与截止时间(`ERR_PNPM_NO_MATURE_MATCHING_VERSION`)。在同一个文件里放行这一个版本: ```yaml minimumReleaseAgeExclude: - '@liustack/modlens@3.17.2' ``` 或者只为这一条命令解除冷静期,注意它解除的是这条命令解析到的所有包,不只 modlens: ```sh npx -y @deepseek-ai/dsh plugin --profile add @liustack/modlens@latest --config.minimumReleaseAge=0 ``` dsh 的 reconcile 会注意到新版本上的 bundle 声明并激活它,随后重启 dsh。用 `npx -y @deepseek-ai/dsh plugin --profile list` 验证。 ## dsh:模型看不到 read_image 工具 插件注册的工具名是 `modlens_read_image`,不是 `read_image`。dsh 的工具注册表是分层的,scoped 层会遮蔽全局层:宿主的 `read_image` 挂在 agent preset 作用域、插件注册在全局层,两者根本不算重名,于是注册静默成功,模型解析到的仍是宿主那个,而它对纯文本模型直接拒绝([#34](https://github.com/liustack/modlens/issues/34))。用自己的名字就没有东西会遮蔽它,模型是通过工具 schema 找到它的,而 schema 每次请求都会送达,与叫什么名字无关。 如果模型仍然看不到,去 harness 日志里搜 `[modlens] ... registration skipped`。想改名就在插件配置行里设 `toolName`,但要挑一个别人没用的:任何已被某个 scoped 工具占用的名字,都会像 `read_image` 一样被遮蔽,那就又绕回本节了。 ```yaml - 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 才会遵循,两种写法任选: ```bash 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` 参数,因为每家厂商给这个开关起的名字都不一样,所以直接传厂商自己的字段: ```bash modlens config set openai.extraBody '{"thinking":{"type":"disabled"}}' modlens -i shot.png --extra-body '{"reasoning_effort":"low"}' # one run only ``` 各家厂商的具体写法、哪些模型完全关不掉,以及怎么确认字段真的生效,见[配置手册](../skills/modlens/references/configure.zh-CN.md#关闭思考)。 ``` extraBody cannot override "messages" for the openai provider ``` 这个字段承载着图片、prompt 和 schema 强制逻辑。把它删掉,只保留厂商的开关字段。网关返回 400 并点名你设置的某个字段,说明那个端点用的是另一种写法。在 `antigravity-cli` 或 `claude-cli` 上运行时,`meta.warnings` 会说明该值被忽略了,因为 CLI provider 没有请求体。 ## Windows ModLens 可以在 Windows 上运行。三个值得了解的平台差异: - **没有 POSIX 权限检查。**Windows 文件没有所有者、组、其他用户的权限位(读出来是 `0o666`/`0o777`,实际访问由 ACL 控制),所以 `doctor` 不评判配置文件的权限模式,`recover-paste --out-dir` 也不会因所有权或组和其他用户的权限而拒绝目录。`--out-dir` 的符号链接检查仍然生效。 - **Harness 检测依赖环境特征。**没有 `ps` 可以读进程树,检测只能依靠各 harness 设置的环境变量。猜错时用 `--harness ` 或 `MODLENS_HARNESS` 强制指定。 - **粘贴恢复。**OpenCode 的恢复在 Windows 上已覆盖(issue #11)。Claude Code 和 Pi 的 JSONL 路径依赖 `os.homedir()` 和各 harness 在那里的磁盘 slug。恢复扑空时,用 `--transcript` 直接指向文件,或把图片拖进终端。 ## 还是没解决 提 issue 时附上完整命令和完整报错:https://github.com/liustack/modlens/issues