opencode-vision-analyze 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,9 +13,9 @@ A tool-based vision routing plugin for [opencode](https://opencode.ai): when the
13
13
  ## Features
14
14
 
15
15
  - **Tool-based, not pre-analysis.** The turn starts immediately; the model decides when (and with which question) to look. No blocking on submit, failures are visible and retryable inside the agent loop. Same philosophy as production-proven agent designs.
16
- - **Question-aware descriptions.** The model passes its own focused question to `vision_analyze` — not a one-shot generic caption computed at submit time.
16
+ - **Question-aware descriptions.** The model passes its own focused question to `vision_analyze` — not a one-shot generic caption computed at submit time. For a general parse of an image the model leaves `question` empty, and the tool falls back to a fixed prompt so all generic descriptions of the same image share one cache entry (see *Content-addressed cache*).
17
17
  - **Native fast path.** If the main model is vision-capable, `vision_analyze` skips the vision model entirely and returns the raw image as a tool attachment.
18
- - **Content-addressed cache.** Images are stored as content-addressed `<sha256>.<ext>` files in a **user-level shared directory** (`<cache>/opencode-vision-analyze/vision`, deduped across projects/sessions, LRU-capped 2000 entries / 500 MB); descriptions are cached per `<image-hash>:<question>` in a **user-level shared directory** (`<cache>/opencode-vision-analyze/descriptions`) the same image with the same question is described exactly once, across projects, processes, and plugin restarts. The description cache is LRU-capped (2000 entries / 50 MB).
18
+ - **Content-addressed cache.** Images and descriptions are stored by content hash in user-level shared dirs and reused across sessions, projects and restarts the same image is never described twice, and all generic full-image parses collapse onto that image's single entry. Storage layout, size limits and the canonical prompt are detailed in *Storage and caches* below.
19
19
  - **Unified auth.** The vision call runs through an opencode sub-session, so it reuses the provider credentials opencode already manages. No extra API key plumbing.
20
20
 
21
21
  ## Installation
@@ -69,10 +69,6 @@ Notes for the curl path:
69
69
 
70
70
  Supported image extensions: png / jpg / jpeg / gif / webp.
71
71
 
72
- Image storage: stored in a **user-level shared directory** `<cache>/opencode-vision-analyze/vision` regardless of git scope — one content-addressed `<sha256>.<ext>` file per unique image, shared across all projects. Pasted images and `http(s)` downloads are written here (atomic temp-file + rename, so concurrent opencode processes can safely share the store); **already-local image paths passed straight to the tool are read in place and never copied**. Defaults per platform: Linux `$XDG_CACHE_HOME || ~/.cache`, macOS `~/Library/Caches` (a `$XDG_CACHE_HOME` override is honored), Windows `%LOCALAPPDATA% || ~/AppData/Local`. An empty cache-root env var is treated as unset (falls back to the default). The store is LRU-capped (2000 entries / 500 MB; oldest by file mtime is evicted when either limit is exceeded), so no `.gitignore` entry is needed anywhere.
73
-
74
- Description cache: stored in a **user-level shared directory** `<cache>/opencode-vision-analyze/descriptions` regardless of git scope — one JSON entry per `<image-sha>:<question>` key, named by `sha256(key)`. It is capped at 2000 entries / 50 MB with LRU eviction (oldest by file mtime is removed when either limit is exceeded). Writes are atomic (temp file + rename), so concurrent opencode processes can safely share the cache.
75
-
76
72
  An ordered-candidates example with auto-fallback and free-first discovery:
77
73
 
78
74
  ```jsonc
@@ -91,6 +87,12 @@ An ordered-candidates example with auto-fallback and free-first discovery:
91
87
  }
92
88
  ```
93
89
 
90
+ ### Storage and caches
91
+
92
+ Image storage: stored in a **user-level shared directory** `<cache>/opencode-vision-analyze/vision` regardless of git scope — one content-addressed `<sha256>.<ext>` file per unique image, shared across all projects. Pasted images and `http(s)` downloads are written here (atomic temp-file + rename, so concurrent opencode processes can safely share the store); **already-local image paths passed straight to the tool are read in place and never copied**. Defaults per platform: Linux `$XDG_CACHE_HOME || ~/.cache`, macOS `~/Library/Caches` (a `$XDG_CACHE_HOME` override is honored), Windows `%LOCALAPPDATA% || ~/AppData/Local`. An empty cache-root env var is treated as unset (falls back to the default). The store is LRU-capped (2000 entries / 500 MB; oldest by file mtime is evicted when either limit is exceeded), so no `.gitignore` entry is needed anywhere.
93
+
94
+ Description cache: stored in a **user-level shared directory** `<cache>/opencode-vision-analyze/descriptions` regardless of git scope — one JSON entry per `<image-sha>:<effective-question>` key, named by `sha256(key)`. `question` is optional on the tool: a general full-image parse (empty or omitted) is normalised to the fixed prompt `Describe this image in full detail, including all text, UI elements, diagrams, or content visible.`, so every such request uses the key `<image-sha>:Describe this image in full detail, including all text, UI elements, diagrams, or content visible.` when its wording normalises to that prompt; specific follow-ups keep their own `<image-sha>:<question>` keys (format unchanged, existing entries keep hitting). Generic entries are only written when the description is long enough (≥ 100 chars), so a short refuse/fail answer can't poison the shared full-image entry. It is capped at 2000 entries / 50 MB with LRU eviction (oldest by file mtime is removed when either limit is exceeded). Writes are atomic (temp file + rename), so concurrent opencode processes can safely share the cache.
95
+
94
96
  ## How it works
95
97
 
96
98
  ```
@@ -114,9 +116,10 @@ vision_analyze tool:
114
116
  ├─ native fast path: session's main model is vision-capable
115
117
  │ → return raw image as attachment (no vision model call)
116
118
  ├─ http(s) image URL → download (20 MB cap) → same disk path
117
- ├─ description cache hit (sha + question) → return cached text
118
- │ (persisted under <cache>/opencode-vision-analyze/descriptions,
119
- tagged with the model that produced it, LRU-capped 2000 entries / 50 MB)
119
+ ├─ description cache hit (image sha + effective question) → return cached text
120
+ │ (general parses converge on <sha>:<canonical full-detail prompt>;
121
+ persisted under <cache>/opencode-vision-analyze/descriptions,
122
+ │ tagged with the model that produced it)
120
123
  └─ candidate chain: sub-session under current session per candidate, in order —
121
124
  parentID, all tools disabled, dedicated system prompt, image + question
122
125
  sent to that vision model → first success returns → sub-session deleted
@@ -131,10 +134,7 @@ Key behaviors:
131
134
  - **Loginless free models** — auto-discovery uses `config.providers()`, the same source as the `/models` picker, so image-capable zen free models are found even without login (their provider is `custom` source → default last tier; put them first with `free_first: true`).
132
135
  - **The tool never throws** — every failure returns readable text so the agent loop can retry, rephrase, or inform the user.
133
136
  - **URL images** — `image_path` accepts `http(s)://...` URLs (must end in a supported image extension: png/jpg/jpeg/gif/webp).
134
-
135
- ## Known limitations
136
-
137
- - **V1 session flow only** — hooks are attached to the V1 `SessionPrompt` path; if opencode's default interaction moves to the V2 session core, hooks won't fire (silently).
137
+ - **General parses share one cache entry** — an empty or omitted `question` is treated as a full-image parse and reuses that image's cached description; specific follow-ups keep their own entries (details in *Storage and caches*).
138
138
 
139
139
  ## Roadmap
140
140
 
package/README.zh.md CHANGED
@@ -13,9 +13,9 @@
13
13
  ## 特性
14
14
 
15
15
  - **工具化,而非提交时预分析。** 轮次即时启动;模型自己决定何时看图、带着什么问题看。提交零阻塞,失败在 agent 循环里可见、可重试。
16
- - **描述针对问题。** 模型把自己关注的问题传给 `vision_analyze`——而不是提交时预生成的一次性通用描述。
16
+ - **描述针对问题。** 模型把自己关注的问题传给 `vision_analyze`——而不是提交时预生成的一次性通用描述。若只是让描述整张图,模型留空 `question`,工具用固定文案兜底,使同一张图的泛描述共享一条缓存(见"内容寻址缓存")。
17
17
  - **原生快速路径。** 主模型本身有视觉能力时,`vision_analyze` 完全跳过视觉模型,直接把原图作为工具附件返回。
18
- - **内容寻址缓存。** 图片按内容寻址 `<sha256>.<ext>` 落盘到**用户级共享目录**(`<cache>/opencode-vision-analyze/vision`,跨项目/会话天然去重,LRU 上限 2000 条 / 500MB);描述按 `<图片哈希>:<问题>` 缓存到**用户级共享目录**(`<cache>/opencode-vision-analyze/descriptions`)——跨项目/进程/插件重启同图同问题只描述一次;描述缓存 LRU 上限 2000 条 / 50MB。
18
+ - **内容寻址缓存。** 图片与描述按内容哈希落到用户级共享目录,跨会话 / 项目 / 重启复用——同一张图不会重复付费描述;同一张图的所有泛解析请求都收敛到同一条缓存。存储路径、容量上限与固定文案详见下文「存储与缓存」。
19
19
  - **统一鉴权。** 视觉调用走 opencode 子会话,复用 opencode 已管理的 provider 凭据,无需额外配置 API Key。
20
20
 
21
21
  ## 安装
@@ -69,10 +69,6 @@ curl 方式说明:
69
69
 
70
70
  受支持的图片扩展名:png / jpg / jpeg / gif / webp。
71
71
 
72
- 图片存储:**恒定放用户级共享目录** `<cache>/opencode-vision-analyze/vision`(与 git / 非 git 分域无关),同一用户所有项目共享;每个唯一图片一个内容寻址 `<sha256>.<ext>` 文件。贴图与 http(s) 下载写入于此(临时文件 + rename 原子写,多个 opencode 进程可安全共享);**模型直接把本地已存在文件路径传给工具时不复制、原位读用**。各平台默认:Linux `$XDG_CACHE_HOME || ~/.cache`;macOS `~/Library/Caches`(亦接受 `$XDG_CACHE_HOME` 覆盖);Windows `%LOCALAPPDATA% || ~/AppData/Local`。缓存根 env 为空串视为未设置(回退默认)。图片缓存 LRU 上限 2000 条 / 500MB,任一超限即按文件 mtime 淘汰最久未用的条目——无需任何 `.gitignore` 条目。
73
-
74
- 描述缓存:**恒定放用户级共享目录** `<cache>/opencode-vision-analyze/descriptions`(与 git / 非 git 分域无关)——每条目一个 JSON 文件,key 为 `<图片sha256>:<问题>`、文件名取 `sha256(key)`。容量上限 2000 条 / 50MB,任一超限即按文件 mtime 淘汰最久未用的条目(LRU)。写入为原子操作(临时文件 + rename),多个 opencode 进程可安全共享同一描述缓存。
75
-
76
72
  有序候选 + 自动续接 + 免费优先的配置示例:
77
73
 
78
74
  ```jsonc
@@ -91,6 +87,12 @@ curl 方式说明:
91
87
  }
92
88
  ```
93
89
 
90
+ ### 存储与缓存
91
+
92
+ 图片存储:**恒定放用户级共享目录** `<cache>/opencode-vision-analyze/vision`(与 git / 非 git 分域无关),同一用户所有项目共享;每个唯一图片一个内容寻址 `<sha256>.<ext>` 文件。贴图与 http(s) 下载写入于此(临时文件 + rename 原子写,多个 opencode 进程可安全共享);**模型直接把本地已存在文件路径传给工具时不复制、原位读用**。各平台默认:Linux `$XDG_CACHE_HOME || ~/.cache`;macOS `~/Library/Caches`(亦接受 `$XDG_CACHE_HOME` 覆盖);Windows `%LOCALAPPDATA% || ~/AppData/Local`。缓存根 env 为空串视为未设置(回退默认)。图片缓存 LRU 上限 2000 条 / 500MB,任一超限即按文件 mtime 淘汰最久未用的条目——无需任何 `.gitignore` 条目。
93
+
94
+ 描述缓存:**恒定放用户级共享目录** `<cache>/opencode-vision-analyze/descriptions`(与 git / 非 git 分域无关)——每条目一个 JSON 文件,key 为 `<图片sha256>:<生效问题>`、文件名取 `sha256(key)`。`question` 参数可选:描述整张图的泛解析(空 / 省略)会归一化到固定文案 `Describe this image in full detail, including all text, UI elements, diagrams, or content visible.`,措辞与固定文案归一化等价时即落在 key `<图片sha>:Describe this image in full detail, including all text, UI elements, diagrams, or content visible.`;具体追问保留原问句走各自的 `<图片sha>:<问题>` key(格式与旧版一致,存量条目照常命中)。泛解析条目要求描述文本 ≥ 100 字符才写入,防止视觉模型敷衍/拒答的短文本毒化整图共享条目。容量上限 2000 条 / 50MB,任一超限即按文件 mtime 淘汰最久未用的条目(LRU)。写入为原子操作(临时文件 + rename),多个 opencode 进程可安全共享同一描述缓存。
95
+
94
96
  ## 工作原理
95
97
 
96
98
  ```
@@ -114,9 +116,10 @@ vision_analyze 工具:
114
116
  ├─ 原生快速路径:会话主模型有视觉能力
115
117
  │ → 原图作为附件直接返回(不调视觉模型)
116
118
  ├─ http(s) 图片 URL → 下载(20 MB 上限)→ 统一磁盘路径
117
- ├─ 描述缓存命中(图片哈希 + 问题)→ 直接返回缓存文本
118
- (持久化于 <cache>/opencode-vision-analyze/descriptions,
119
- 标签沿用产出该描述的模型;LRU 上限 2000 条 / 50MB)
119
+ ├─ 描述缓存命中(图片哈希 + 生效问题)→ 直接返回缓存文本
120
+ (泛解析收敛到 <sha>:<固定整图描述文案>;
121
+ 持久化于 <cache>/opencode-vision-analyze/descriptions,
122
+ │ 标签沿用产出该描述的模型)
120
123
  └─ 候选链:沿链逐候选建子会话(parentID 挂当前会话、禁用全部工具、
121
124
  专用 system prompt,图片 + 问题发给该候选视觉模型)
122
125
  → 首个成功即返回 → 子会话删除
@@ -131,10 +134,7 @@ vision_analyze 工具:
131
134
  - **免登录免费模型** —— 自动发现与 `/models` 选择器同源(`config.providers()`),免登录也可发现的 zen free 视觉模型会进入候选链(其 provider 为 `custom` 源 → 默认最末档;`free_first: true` 可提到最前)。
132
135
  - **工具永不抛错** —— 所有失败都返回可读文字,agent 循环可以重试、换问题或告知用户。
133
136
  - **URL 图片** —— `image_path` 接受 `http(s)://...` 地址(需以受支持的图片扩展名结尾:png/jpg/jpeg/gif/webp)。
134
-
135
- ## 已知限制
136
-
137
- - **仅 V1 会话流** —— 钩子挂在 V1 `SessionPrompt` 路径上;若 opencode 默认交互切到 V2 会话核心,钩子不会触发(且不报错)。
137
+ - **泛解析共享同一条缓存** —— 空 / 省略的 `question` 视为整图描述,复用该图已缓存的描述;具体追问各自保留条目(细节见「存储与缓存」)。
138
138
 
139
139
  ## Roadmap
140
140
 
package/dist/index.d.ts CHANGED
@@ -48,6 +48,37 @@ export declare const visionCacheLimits: {
48
48
  maxEntries: number;
49
49
  maxBytes: number;
50
50
  };
51
+ /**
52
+ * 泛解析(描述整张图)用的固定问题文案。
53
+ *
54
+ * 主模型对「解析这张图 / 描述图片」这类请求应省略 question,由工具用这里的文案兜底,
55
+ * 从而让所有泛解析请求的缓存 key 收敛到同一条(key = <图片sha>:GENERIC_QUESTION),
56
+ * 避免主模型每次自编措辞把缓存拆成多份、跨会话永远命中不了。文案点名文字/UI/图表/
57
+ * 可见内容,驱动视觉模型把整图信息尽量带全。
58
+ *
59
+ * 这份文案相当于公开契约:改动它会让旧的泛解析缓存条目失去命中(由 LRU 按 mtime 清理),
60
+ * 属于刻意为之——将来想改泛描述措辞、或按用户/场景定制时,改文案即可自然分开新旧缓存。
61
+ */
62
+ export declare const GENERIC_QUESTION = "Describe this image in full detail, including all text, UI elements, diagrams, or content visible.";
63
+ /**
64
+ * 泛解析缓存条目的最短文本长度(字符)。
65
+ *
66
+ * canonical 描述要求整图加逐字转录,正常结果不可能太短;太短多半是视觉模型敷衍或拒答。
67
+ * 泛解析条目是全图共享的,存进一条垃圾描述会让之后所有泛解析都命中它,所以写入时长度
68
+ * 低于该值就跳过。具体追问不受限制(“答案是 42”是合法答案)。
69
+ * 做成模块级可变对象,测试注入大值/小值验证两侧行为。
70
+ */
71
+ export declare const genericWriteMinText: {
72
+ chars: number;
73
+ };
74
+ /**
75
+ * 归一化问题文本,供「是否为泛解析」判定使用:
76
+ * 去掉首尾空白与引号/括号,把连续空白(含全角/换行)折成一个空格,去掉句末标点,统一小写。
77
+ * 只做字符级归一化,不做语义匹配——误判泛解析会把针对性回答套到错误语义上,宁可 miss。
78
+ */
79
+ export declare function normalizeQuestion(q: string): string;
80
+ /** 是否泛解析请求:没给问题(空/纯空白),或归一化后与 GENERIC_QUESTION 一致。 */
81
+ export declare function isGenericQuestion(q: string): boolean;
51
82
  declare const _default: {
52
83
  id: string;
53
84
  server: Plugin;
package/dist/index.js CHANGED
@@ -10,10 +10,14 @@
10
10
  * 工作方式(vision_analyze 工具路径):主模型调用 vision_analyze 时,插件
11
11
  * 创建一个 parentID 挂在当前会话下的临时子会话(不进会话列表、不生成
12
12
  * 标题、禁用全部工具),把原图以 data URL 发给视觉模型,取回描述文字后
13
- * 删除子会话并返回描述。同一张图 + 同一问题的描述按「<图片sha256>:<问题>」
14
- * 键缓存到**用户级共享目录**(<cache>/opencode-vision-analyze/descriptions
15
- * 每条目一文件、mtime 作 LRU 时钟、2000 条 / 50MB 双上限、单条超限不入缓存)——
16
- * 跨项目/跨进程/插件重启后同图同问题只描述一次。
13
+ * 删除子会话并返回描述。描述按「<图片sha256>:<问题>」键缓存到**用户级共享目录**
14
+ * (<cache>/opencode-vision-analyze/descriptions,每条目一文件、mtime 作 LRU
15
+ * 时钟、2000 条 / 50MB 双上限、单条超限不入缓存)——跨项目/跨进程/插件重启后
16
+ * 同图同问题只描述一次。
17
+ * 泛解析(描述整图)请求的 question 会归一化到固定文案 GENERIC_QUESTION
18
+ * (主模型省略 question 即走该默认值),key 收敛为 <图片sha>:GENERIC_QUESTION,
19
+ * 避免主模型每次自编措辞把缓存拆碎;有具体追问时保留原问句走各自精确 key。
20
+ * 泛解析条目是全图共享的,写入前按最短文本长度把关,防一条垃圾描述毒化整图。
17
21
  * image_path 除了绝对路径也接受 http(s) URL:先下载落盘到同一用户级 vision
18
22
  * 目录(内容哈希命名,天然与附件落盘去重;LRU + 容量上限,2000 条 / 500MB),
19
23
  * 再走统一的磁盘加载路径。**本地已存在的文件直接原位读取**(不复制、不 touch),
@@ -130,6 +134,45 @@ export const visionCacheLimits = {
130
134
  maxEntries: 2000,
131
135
  maxBytes: 500 * 1024 * 1024,
132
136
  };
137
+ /**
138
+ * 泛解析(描述整张图)用的固定问题文案。
139
+ *
140
+ * 主模型对「解析这张图 / 描述图片」这类请求应省略 question,由工具用这里的文案兜底,
141
+ * 从而让所有泛解析请求的缓存 key 收敛到同一条(key = <图片sha>:GENERIC_QUESTION),
142
+ * 避免主模型每次自编措辞把缓存拆成多份、跨会话永远命中不了。文案点名文字/UI/图表/
143
+ * 可见内容,驱动视觉模型把整图信息尽量带全。
144
+ *
145
+ * 这份文案相当于公开契约:改动它会让旧的泛解析缓存条目失去命中(由 LRU 按 mtime 清理),
146
+ * 属于刻意为之——将来想改泛描述措辞、或按用户/场景定制时,改文案即可自然分开新旧缓存。
147
+ */
148
+ export const GENERIC_QUESTION = "Describe this image in full detail, including all text, UI elements, diagrams, or content visible.";
149
+ /**
150
+ * 泛解析缓存条目的最短文本长度(字符)。
151
+ *
152
+ * canonical 描述要求整图加逐字转录,正常结果不可能太短;太短多半是视觉模型敷衍或拒答。
153
+ * 泛解析条目是全图共享的,存进一条垃圾描述会让之后所有泛解析都命中它,所以写入时长度
154
+ * 低于该值就跳过。具体追问不受限制(“答案是 42”是合法答案)。
155
+ * 做成模块级可变对象,测试注入大值/小值验证两侧行为。
156
+ */
157
+ export const genericWriteMinText = { chars: 100 };
158
+ /**
159
+ * 归一化问题文本,供「是否为泛解析」判定使用:
160
+ * 去掉首尾空白与引号/括号,把连续空白(含全角/换行)折成一个空格,去掉句末标点,统一小写。
161
+ * 只做字符级归一化,不做语义匹配——误判泛解析会把针对性回答套到错误语义上,宁可 miss。
162
+ */
163
+ export function normalizeQuestion(q) {
164
+ return q
165
+ .trim()
166
+ .replace(/^[\s"'“”「」『』()()\[\]]+|[\s"'“”「」『』()()\[\]]+$/g, "")
167
+ .replace(/[\s]+/g, " ")
168
+ .replace(/[.。!!??]+$/g, "")
169
+ .toLowerCase();
170
+ }
171
+ /** 是否泛解析请求:没给问题(空/纯空白),或归一化后与 GENERIC_QUESTION 一致。 */
172
+ export function isGenericQuestion(q) {
173
+ const s = (q ?? "").trim();
174
+ return s === "" || normalizeQuestion(s) === normalizeQuestion(GENERIC_QUESTION);
175
+ }
133
176
  /**
134
177
  * http(s) 下载图片的大小上限(20 MB)。提示注入可让模型指向超大图片,
135
178
  * 下载不限长是成本/健壮性放大器:先按 content-length 头提前拒绝,
@@ -533,7 +576,13 @@ const plugin = async (input, optionsArg) => {
533
576
  const visionAnalyze = async (args, ctx) => {
534
577
  const title = "vision_analyze";
535
578
  try {
536
- const question = args.question?.trim() || "Describe this image in full detail.";
579
+ // 泛解析判定:没给问题(空/纯空白),或措辞与 GENERIC_QUESTION 归一化一致。
580
+ // 泛解析请求统一改写为 GENERIC_QUESTION——缓存 key 固定成 <图片sha>:GENERIC_QUESTION,
581
+ // 跨会话、跨措辞都能命中;主模型若问的是具体对象/文字/区域/颜色,则保留原问句,
582
+ // 走各自的精确 key(与旧版本格式一致,存量条目不受影响)。
583
+ const trimmed = args.question?.trim() ?? "";
584
+ const generic = trimmed === "" || isGenericQuestion(trimmed);
585
+ const question = generic ? GENERIC_QUESTION : trimmed;
537
586
  // http(s) URL:先下载到用户级 vision 缓存目录,再统一走磁盘加载路径。
538
587
  // ctx.abort 传入下载:用户中止即刻断下载(含 pre-abort 不再发请求)。
539
588
  const download = /^https?:\/\//i.test(args.image_path)
@@ -563,7 +612,7 @@ const plugin = async (input, optionsArg) => {
563
612
  ],
564
613
  };
565
614
  }
566
- // 描述缓存:内容哈希 + 问题作为 key,命中直接复用(title 标注 cached)。
615
+ // 描述缓存:内容哈希 + 生效问题作为 key,命中直接复用(title 标注 cached)。
567
616
  // 落盘在用户级共享目录(resolveDescriptionDir)——跨项目/进程/重启命中;磁盘即事实。
568
617
  const key = `${createHash("sha256").update(image.bytes).digest("hex")}:${question}`;
569
618
  const cached = await descCacheGet(key);
@@ -575,6 +624,12 @@ const plugin = async (input, optionsArg) => {
575
624
  const result = await describeWithChain(image, question, ctx);
576
625
  if (!result.ok)
577
626
  return { title, output: `Image analysis failed: ${result.error}` };
627
+ // 泛解析条目是全图共享的:文本太短多半是视觉模型敷衍/拒答,存进去会毒化这条
628
+ // canonical 缓存,让以后所有泛解析都命中垃圾描述,所以低于门槛就跳过落盘。
629
+ // 具体追问不受限("答案是 42" 是合法短答案)。
630
+ if (generic && result.text.length < genericWriteMinText.chars) {
631
+ return { title, output: format(path.basename(imagePath), result.modelId, result.text) };
632
+ }
578
633
  // 入库带上实际产出描述的候选 modelId,供后续缓存命中还原真实标签
579
634
  await descCacheSet(key, { modelId: result.modelId, text: result.text });
580
635
  // 成功标签直接用实际产出描述的候选引用键(而非链首近似)
@@ -745,6 +800,10 @@ const plugin = async (input, optionsArg) => {
745
800
  }
746
801
  if (lines.length === 0)
747
802
  return;
803
+ // 泛解析指引:主模型对"描述整图/解析图片"这类请求应省略 question(工具内部会用固定
804
+ // canonical 文案兜底,缓存 key 才能跨会话收敛);只有用户问具体对象/文字/区域/颜色时才
805
+ // 填具体问句。措辞分化的泛问句会让缓存 key 拆碎、永远命中不了。
806
+ lines.push("[When the user asks for a general parse of the whole image, call vision_analyze WITHOUT a question (omit the question parameter). Only pass a question when the user asks about a specific object, text, region, or color.]");
748
807
  // 注入 synthetic text part:TUI 隐藏(不干扰用户输入展示),但会发给模型。
749
808
  // id 需满足 PartID 约定(prt 前缀)。
750
809
  const hint = {
@@ -765,10 +824,13 @@ const plugin = async (input, optionsArg) => {
765
824
  // 既不引入 zod 运行时依赖(本插件只用 node 内置模块),也不使用 any。
766
825
  tool: {
767
826
  vision_analyze: {
768
- description: "Analyze an image with the dedicated vision model. image_path is an absolute file path (as given in the user's attachment hint) or an http(s) image URL. question describes what to look for; be specific.",
827
+ description: "Analyze an image with the dedicated vision model. image_path is an absolute file path (as given in the user's attachment hint) or an http(s) image URL. question is optional: pass it only when you need a specific detail (an object, text, region or color); otherwise omit it to get a full description of the whole image.",
769
828
  args: {
770
829
  image_path: { type: "string", description: "Absolute path to the image file, or an http(s) image URL." },
771
- question: { type: "string", description: "What to look for or answer about the image." },
830
+ question: {
831
+ type: "string",
832
+ description: "Optional. What specific detail to look for (object/text/region/color). Omit for a full description of the whole image.",
833
+ },
772
834
  },
773
835
  execute: visionAnalyze,
774
836
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-vision-analyze",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "OpenCode plugin: vision_analyze tool — describe images & image URLs with your own vision model for text-only main models; native-image fast path for multimodal models. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",