toRolex
periscope
Bridge vision to text-only coding agents — a Claude Code & Codex plugin for DeepSeek and friends
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 5, 2026
- Updated
- Aug 12, 2026
Introduction
periscope
给纯文本 coding agent 的视觉桥插件(MVP)。把本地图片 / 远程图片 URL / 用户在 Claude Code 里的贴图,转成文字描述,喂给只吃文本的 agent。零构建、零额外运行时依赖,dist/ 已随仓库提交。
periscope 采用 BYOM(bring your own model) 定位:不绑定任何服务商,视觉模型完全由你自带——本地 Ollama / LM Studio、自建网关、任意 OpenAI 兼容云端端点皆可。运行 init 向导填入你的 baseUrl / model(apiKey 可留空)即可接入(见「配置」)。
image ──▶ describe.js ──▶ 外部视觉 LLM(openai / anthropic / responses)──▶ 文字描述
特性
- 独立脚本:
node dist/cli/describe.js <图片路径或URL> [...],单图输出纯文本,多图逐行${source}: ${描述}。 - 三协议:openai(chat/completions,DashScope 兼容)、anthropic(v1/messages)、responses(v1/responses),配置文件切换。
- 贴图 hook:Claude Code
UserPromptSubmit事件自动读取image_paths,把[Image N] basename: 描述注入additionalContext,始终放行(decision: approve)。 - 本地缓存:未变图片的描结果缓存到
~/.cache/periscope/,命中不重复请求视觉端点;远程 URL 图不落缓存。 - 零构建:编译产物
dist/随仓库提交,用户拿到即用,无需安装 typescript 或运行 build。
安装
作为 Claude Code 插件(推荐)
claude plugin marketplace add toRolex/periscope
claude plugin install periscope
或在 Claude Code 内用 /plugin 添加 marketplace toRolex/periscope 后安装 periscope。装好后贴图自动注入描述,也可用 /describe-image skill 手动触发。
安装后配置
装好后先初始化配置,让 describe 能连上你的视觉端点。在 Claude Code 会话里敲 /set-up(仅用户主动触发,模型不会擅自弹出配置流程),skill 会引导你在独立终端运行 init 脚本完成配置、在会话内解释协议与 apiKey 选项,最后自动跑 doctor 验证配置可被正确读取。
也可以跳过 /set-up,直接在独立终端手动运行 init 脚本(见下文「init」一节):
node dist/cli/init.js
作为独立脚本使用
前置要求:Node.js >= 20。
# git clone 后直接使用(dist/ 已提交,无需 build)
git clone <仓库地址>
cd periscope
pnpm install # 仅安装 typescript(开发用);纯使用可跳过
node dist/cli/describe.js ./demo.png
纯使用场景不需要
pnpm install,直接跑dist/下的编译产物即可。pnpm install/pnpm build只在需要修改源码或跑测试时需要。
配置
配置文件路径:~/.config/periscope/config.json。首次运行任意命令时会懒创建空白模板配置(见下),需用 init 向导或手改填入你自己的视觉端点;用环境变量 PERISCOPE_CONFIG 可覆盖配置路径。
periscope 是 BYOM(bring your own model) 插件:不绑定任何服务商,不默认指向任何供应商端点。baseUrl / model 由你提供,通过 node dist/cli/init.js 交互式向导填写(apiKey 可留空,见「init」一节);本地 Ollama / LM Studio / 自建网关 / 任意云端端点皆可接入。
首次懒创建写入的空白模板(三协议 baseUrl / model 均为空串,不绑定任何服务商):
{
"protocol": "openai",
"apiKey": "",
"openai": {
"baseUrl": "",
"model": ""
},
"anthropic": {
"baseUrl": "",
"model": ""
},
"responses": {
"baseUrl": "",
"model": ""
}
}
填入端点后示例(以 DashScope 兼容模式为例):
{
"protocol": "openai",
"apiKey": "sk-xxx",
"openai": {
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"model": "qwen-vl-max"
},
"anthropic": {
"baseUrl": "https://api.anthropic.com",
"model": "claude-3-5-sonnet-latest"
},
"responses": {
"baseUrl": "https://api.openai.com/v1",
"model": "gpt-4o-mini"
}
}
三协议
protocol 字段决定当前使用的协议适配器,每个协议都有独立的 baseUrl 与 model 字段:
| protocol | 请求端点 | 鉴权方式 | 示例 model |
|---|---|---|---|
openai | {baseUrl}/chat/completions | Authorization: Bearer | qwen-vl-max |
anthropic | {baseUrl}/v1/messages | x-api-key + anthropic-version: 2023-06-01 | claude-3-5-sonnet-latest |
responses | {baseUrl}/responses | Authorization: Bearer | gpt-4o-mini |
- openai(默认协议,仅指请求形状):兼容 OpenAI chat/completions 格式的端点都可用;
baseUrl/model由用户自行配置,不默认指向任何服务商。 - anthropic:走 Anthropic Messages API,图片以 base64 的
imagecontent block 发送。 - responses:走 OpenAI Responses API,图片以
input_image块发送。
切换协议示例:把 config.json 的 protocol 改成 "anthropic",并填上对应的 apiKey 与(如有需要)baseUrl / model。未知协议会报错并列出可用值(openai, anthropic, responses)。未配置端点时 describe 会报错并提示运行 init。
环境变量
| 环境变量 | 作用 | 优先级 |
|---|---|---|
PERISCOPE_API_KEY | 视觉端点 API key | 优先于配置文件 apiKey |
PERISCOPE_CONFIG | 配置文件路径(默认 ~/.config/periscope/config.json) | 覆盖默认路径 |
PERISCOPE_CACHE_DIR | 缓存目录(默认 ~/.cache/periscope/) | 覆盖默认目录 |
独立脚本用法
describe — 描述图片
node dist/cli/describe.js <图片路径或URL> [...] [--intent ocr|table|chart|"描述内容"]
<图片路径或URL>:本地图片路径或http(s)图片 URL,可传多个,空格分隔。--intent ...(可选):内置任务模板名ocr/table/chart(见下「任务模板」),命中模板名时使用内置 prompt;其他文本原样透传给模型,如--intent "读取图片中的报错信息";缺省保持默认描述文案。- 插件环境里用
node ${CLAUDE_PLUGIN_ROOT}/dist/cli/describe.js <图片路径或URL> [...]。
describe 任务模板(ocr / table / chart)
--intent 支持三个内置命名任务模板,命中模板名时使用内置 prompt:
| 模板名 | 作用 | 内置 prompt |
|---|---|---|
ocr | 提取图片中的全部文字 | 提取图片中的全部文字内容 |
table | 把图片中的表格转换为 Markdown 表格 | 把图片中的表格转换为 Markdown 表格 |
chart | 把图片中的图表转换为结构化文字描述 | 把图片中的图表转换为结构化文字描述 |
# 用法示例:模板名作 --intent 参数
node dist/cli/describe.js ./截图.png --intent ocr
node dist/cli/describe.js ./表格.png --intent table
node dist/cli/describe.js ./柱状图.png --intent chart
非模板文本原样透传给模型,如 --intent "读取图片中的报错信息";缺省(不传 --intent)保持默认描述文案。
init — 交互式初始化配置
node dist/cli/init.js
在**独立终端(TTY)**运行的交互式向导:↑/↓ 方向键选择协议(openai / anthropic / responses,回车确认)→ 逐项填写 baseUrl / model(必填)与 apiKey(可留空)→ 展示配置摘要(已存在配置时附覆盖警告)→ 输入 y 确认覆盖写入 / 其他键放弃。写入路径 PERISCOPE_CONFIG 优先,否则 ~/.config/periscope/config.json。
行为要点:
- 确认覆盖:目标文件已存在时先展示摘要 + 覆盖警告,输入
y才覆盖写入;输入其他字符放弃写入,现有配置保持不变。没有默认值——baseUrl/model需要用户输入;apiKey可留空(本地无鉴权端点无需 key)。 baseUrl/model必填,空输入报错退出;apiKey可选,直接回车留空即可(Ollama / LM Studio 等本地端点不鉴权,无需填 key;留空写入后 describe 不发送鉴权头)。(EOF 或 Ctrl+C 也立即终止,非零退出码。)- 非 TTY(管道/重定向)环境拒绝运行,报错提示需要在交互式终端中运行。
- 写出的 JSON 包含
protocol/apiKey/openai/anthropic/responses顶层字段;用户选中的协议段baseUrl/model取用户输入,其余协议段保留空白模板(空串)。
# 典型使用:装好插件后首次跑
node dist/cli/init.js
# 选择协议(↑/↓ 切换,回车确认):
# ❯ openai
# anthropic
# responses
# 请输入 baseUrl: http://localhost:11434/v1
# 请输入 model: qwen2.5-vl
# 请输入 apiKey: (本地无鉴权端点直接回车留空)
# 配置摘要:
# 协议: openai
# baseUrl: http://localhost:11434/v1
# model: qwen2.5-vl
# apiKey:
# 确认写入?(y/n): y
# 已写入配置: /Users/you/.config/periscope/config.json
doctor — 本地自检
node dist/cli/doctor.js [--offline]
六项自检,全部纯本地(--offline 时连 schema 网络拉取也禁用):
- config 文件:检查默认路径(
PERISCOPE_CONFIG/~/.config/periscope/config.json)文件存在性。 - 协议段:检查
config.json的openai/anthropic/responses段都有baseUrl+model。 - 激活协议:检查
config.protocol对应的激活协议段baseUrl/model非空,为空时 ❌ 并引导运行init。 - Node 版本:与仓库
package.json的engines.node比较(默认>=20)。 - dist/ 编译产物:检查
dist/cli/describe.js+dist/cli/init.js+dist/cli/doctor.js存在(零构建即用假设)。 - 根
plugin.jsonschema 合规:按 Agent Plugins 1.0.0 校验仓库根plugin.json。
逐项输出 ✅ / ⚠️ / ❌ + 一行结论;❌ 项数 = 退出码是否为零。
--offline 标志语义:禁止 schema 项发起任何外部网络请求(满足 issue #12「全程不发请求」的承诺)。
- 缓存有效(7 天 TTL 内)→ 用本地缓存校验,输出
✅ 根 plugin.json schema 合规(schema 来源: 本地缓存)。 - 缓存缺失或过期 → 降级
⚠️ 离线模式:schema 未缓存,跳过校验(可先联网跑一次 doctor 预热缓存),不发任何 fetch。 - 其余 5 项本地自检不受
--offline影响,仍照常输出。
默认行为(不传 --offline):冷缓存时 schema 项会拉一次远程 schema(成功后续命中本地缓存 7 天);拉取失败降级 ⚠️,不硬失败。
输出与退出码
- 单图:stdout 输出纯文本描述,无前缀。
- 多图:逐行输出
${source}: ${描述},顺序与传入顺序一致,并行请求(总耗时约等于最慢单图)。 - 成功:退出码
0,stdout 为描述。 - 失败(缺参数 / 文件不存在 / 端点非 2xx 等):错误信息走 stderr,退出码非零。
# 单图 + 意图
node dist/cli/describe.js ./screenshot.png --intent "读取图片里的报错信息"
# 多图(本地 + URL 混用)
node dist/cli/describe.js ./a.png https://example.com/cat.png
# 远程 URL 图直接透传给视觉端点,无需先下载
node dist/cli/describe.js https://example.com/diagram.png
贴图 hook(Claude Code 插件)
装成 Claude Code 插件后,你贴图(或让 agent 引用本地图片 / URL)时,periscope 会自动调用视觉模型并把描述注入上下文。插件根 = 仓库根,目录结构即插件结构:
.
├── .claude-plugin/plugin.json # 插件元数据(name: periscope)
├── hooks/hooks.json # UserPromptSubmit hook(exec form)
├── skills/describe-image/SKILL.md # describe-image skill(allowed-tools 规则)
├── dist/ # 编译产物(随仓库提交,零构建)
└── src/ # TypeScript 源码
- 把仓库放进 Claude Code 的插件目录(或通过 marketplace 安装,见「Marketplace 发布说明」)。
hooks/hooks.json声明UserPromptSubmit事件,exec form 执行node ${CLAUDE_PLUGIN_ROOT}/dist/hook/index.js。- hook 读取事件 JSON 的
image_paths,并行描述各图,把[Image N] basename: 描述逐行注入additionalContext:
{
"decision": "approve",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "[Image 1] a.png: 一只猫在窗台\n[Image 2] b.png: 描述不可用"
}
}
行为要点:
- 始终放行:
decision恒为approve,绝不用图片描述失败阻塞消息发送。 - 单图失败注入占位符:该图显示
描述不可用,不影响其余图片。 - 字符预算:
additionalContext软预算约 9000 字符,接近上限时截断并注明(另有 N 张图片未描述)。 - 缓存复用:同一图片(路径 + 修改时间 + 大小未变)多次贴图只请求一次视觉端点。
- 无图片事件注入空串
additionalContext(满足 2.1.x hook schema 必填约束)。 - 在 Claude Code 里也可手动触发 skill:
describe-image,运行node ${CLAUDE_PLUGIN_ROOT}/dist/cli/describe.js <图片路径或URL> [--intent "..."]。
人工实测指南(真实视觉 LLM)
当前仓库环境没有真实视觉 LLM 的 API key / endpoint,AC3「真实视觉 LLM 端到端人工实测」需用户按本指南自行执行。仓库提供了离线 mock 视觉端点(
src/testing/mock-server.ts)供自动化冒烟替代验证(见「开发」)。
按以下步骤用真实端点做端到端实测:
- 配置:编辑
~/.config/periscope/config.json,或设置环境变量。示例(用PERISCOPE_API_KEY与配置文件并存的推荐方式):export PERISCOPE_API_KEY=<你的真实 key> # 先用 `node dist/cli/init.js` 或手改 config.json 填入端点;openai 协议不绑定任何服务商 # 未配置端点时 describe 会报错并提示运行 init - CLI 实测:准备一张本地图片与一个真实 URL,分别跑:
预期结果:stdout 输出与图片内容一致的中文描述;退出码node dist/cli/describe.js ./本地图.png --intent "描述这张图片" node dist/cli/describe.js https://example.com/远程图.png0;再次跑同一张本地图应命中缓存(秒出,不再请求端点)。 - 多图实测:
node dist/cli/describe.js ./a.png ./b.png,预期逐行输出a.png: .../b.png: ...。 - 贴图注入实测:在装好插件 + 配好 key 的 Claude Code 会话里贴一张图,确认:
- agent 能读到
[Image N] basename: 描述形式的图片描述并据此作答; - 消息正常发送(hook 始终
approve,即使某图失败也只显示描述不可用)。
- agent 能读到
- 协议切换实测:把
protocol改为anthropic或responses,配好对应 key 重跑步骤 2,确认响应质量。
预期响应质量:描述应与图片内容吻合(物体、场景、文字、图表要点),--intent 能引导输出侧重点(如「读取文字」应准确转写图片内文字)。
Marketplace 发布说明
插件结构与 marketplace 目录已就绪(claude plugin validate ./ 通过,本地安装验证通过),用户可直接通过 marketplace 安装(见上文「安装」)。发布相关说明:
- 自托管 marketplace(当前形态):仓库
.claude-plugin/marketplace.json声明periscope插件(source 指向本仓库),安装方式:claude plugin marketplace add toRolex/periscope claude plugin install periscope - 版本与 tag:插件
version当前0.1.0。发布新版时用claude plugin tag .生成periscope--v<version>标签并推送,marketplace 用户升级后拉到新版本。 - 提交社区 marketplace(可选):插件成熟后提交到 anthropics/claude-plugins-community 公共目录,让所有 Claude Code 用户可发现。提交前先
claude plugin validate ./通过;社区目录会 pin 插件提交 SHA。 - 发布前建议在干净环境(新 clone + 仅
node)实测「零构建即用」链路,避免漏提交dist/产物。
Agent Plugins 1.0.0 合规
periscope 同时遵守 Agent Plugins 1.0.0 标准(vercel + openai 联合发布的打包格式),被下列兼容 harness 作为标准插件目录加载:
- VS Code
- ChatGPT / Codex
- Kiro
- GitHub Copilot
- Cursor
Claude Code 不在 Agent Plugins 兼容客户端列表里——它读自己的
.claude-plugin/plugin.json与hooks/hooks.json,不读根plugin.json。两套结构互不冲突,Claude Code 体验不变。
合规要点
- 根
plugin.json:仓库根的标准 manifest,包含$schema/name/version/description/author五字段,name沿用periscope。 - Skill 路径:describe 能力以
skills/describe-image/SKILL.md形式承载(frontmattername/description/allowed-tools已在 Agent Skills 规范字段表内,无需改动)。兼容 harness 的 agent 读到 Skill 后按指令调node dist/cli/describe.js <图片路径或URL> [--intent "..."]。 - 不上 MCP server:periscope 不写
mcp.json、不把 describe 暴露为 MCP tool——避免在兼容 harness 工具列表里多一个 describe 噪音;视觉能力以 Skill 文本指令形式呈现。 - Claude Code 原生结构保留:
.claude-plugin/plugin.json+hooks/hooks.json+ 现有skills/路径不动。
常见问题(FAQ)
Q:图片描述缓存存在哪里?怎么清除?
默认 ~/.cache/periscope/,每张图一个 <sha256>.txt 文件。清掉整个目录即可全部失效:rm -rf ~/.cache/periscope。也可用 PERISCOPE_CACHE_DIR 指到别处。
Q:为什么远程 URL 图片不走缓存?
缓存 key 依赖本地文件的路径 + 修改时间 + 大小(sha256(绝对路径+mtime+size)),远程 URL 内容可变且无本地 stat,因此不落缓存、请求体直接透传 URL 给视觉端点。
Q:配置文件在哪?没配置会怎样?
默认 ~/.config/periscope/config.json。首次运行自动懒创建空白模板(protocol: openai,三协议 baseUrl / model 为空串,apiKey 为空),不绑定任何服务商。没运行 init / 没填端点时,describe 会报「协议 X 未配置 baseUrl/model,请运行 init」并提示先配置。没配 apiKey 时请求不带鉴权头,真实端点通常会返回 401;本地 mock 端点不受影响。
Q:hook 失败会阻塞消息发送吗?
不会。decision 恒为 approve;单图失败注入 描述不可用,解析/读取 stdin 失败也放行。附加的 additionalContext 只是上下文增强,不是发送门禁。
Q:CLI 报错长什么样?
错误信息走 stderr,退出码非零。常见:缺图片路径(缺少图片路径)、文件不存在(无法读取图片文件)、端点非 2xx(视觉端点返回 HTTP 500: ...)、未知参数(未知参数: --xxx)、未知协议(未知协议: ...)。
Q:需要安装 TypeScript / 运行 build 吗?
纯使用不需要。dist/ 已随仓库提交,直接跑 node dist/cli/describe.js 即可。pnpm install / pnpm build 只在改源码或跑测试时需要。
Q:Node 版本要求?
Node.js >= 20(代码与测试使用内建 fetch 与 node:test)。
开发
- 依赖极简:devDependencies 仅
typescript(@types/node不引入,手写 ambient 声明在src/global.d.ts)。 - 构建:
pnpm build(=tsc)。 - 测试:
pnpm test(=tsc && node --test),含 CLI、core、三协议、config、缓存、hook、插件契约,以及src/delivery.smoke.test.ts的 mock 端点端到端冒烟。 - 本地 mock 视觉端点:
src/testing/mock-server.ts(离线 HTTP server,记录请求、返回可定制的视觉响应),src/testing/fixtures.ts(1x1 PNG 与临时配置/目录工具)。 - 目录速览:
src/cli/describe.ts— describe 脚本入口(参数解析 → describe → 输出/退出码)src/core/describe.ts— 协议无关核心describe()/describeMany()(缓存 → 转 data URL → 适配器 → 传输)src/protocols/{openai,anthropic,responses,index}.ts— 三协议适配器(请求构造 + 容错响应提取)src/config/config.ts— 懒创建配置 + 环境变量覆盖src/cache.ts— 图片描述本地缓存src/hook/index.ts— UserPromptSubmit hook 桥接