opencode-vision-analyze 0.5.0 → 0.6.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
@@ -89,9 +89,34 @@ An ordered-candidates example with auto-fallback and free-first discovery:
89
89
 
90
90
  ### Storage and caches
91
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.
92
+ Two **user-level shared caches** live side by side under `<cache>/opencode-vision-analyze/`, shared across sessions, projects and restarts, independent of git scope no `.gitignore` entry is needed anywhere.
93
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.
94
+ | Cache | Directory | Content | Naming | Cap |
95
+ |---|---|---|---|---|
96
+ | Images | `vision/` | image bytes | `<sha256>.<ext>` | 2000 entries / 500 MB |
97
+ | Descriptions | `descriptions/` | description text (JSON) | `sha256(key)` | 2000 entries / 50 MB |
98
+
99
+ **Image storage (`vision/`)**
100
+
101
+ - **Written to cache**: clipboard images (raw pixels, no source path) and `http(s)` downloads.
102
+ - **Read in place (never copied)**: path-pasted attachments (the message part carries a real `source.path`, e.g. a file path copied to the clipboard) and already-local paths passed straight to the tool — the file is re-read at analysis time, so re-pasting a path always analyses the latest content.
103
+ - **Eviction**: LRU by file mtime; when either 2000 entries or 500 MB is exceeded, the oldest entries are removed.
104
+ - **Concurrency**: atomic temp-file + rename, so concurrent opencode processes can safely share the store.
105
+
106
+ **Description cache (`descriptions/`)**
107
+
108
+ - **Key**: `<image-sha>:<effective-question>`, filename `sha256(key)`.
109
+ - **General full-image parse** (empty / omitted `question`): normalised to the fixed prompt `Describe this image in full detail, including all text, UI elements, diagrams, or content visible.`, so every generic parse collapses onto one entry.
110
+ - **Specific follow-ups**: keep their own `<image-sha>:<question>` keys (format unchanged, existing entries keep hitting).
111
+ - **Write threshold**: generic entries are only written when the description is ≥ 100 chars, so a short refuse/fail answer can't poison the shared full-image entry.
112
+ - **Eviction**: LRU by file mtime, capped at 2000 entries / 50 MB; **concurrency** as above.
113
+
114
+ **Default cache root per platform**
115
+
116
+ - Linux: `$XDG_CACHE_HOME || ~/.cache`
117
+ - macOS: `~/Library/Caches` (a `$XDG_CACHE_HOME` override is honored)
118
+ - Windows: `%LOCALAPPDATA% || ~/AppData/Local`
119
+ - An empty cache-root env var is treated as unset (falls back to the default).
95
120
 
96
121
  ## How it works
97
122
 
@@ -102,9 +127,11 @@ User pastes image + question
102
127
  ├─ main model has image input capability → do nothing (raw image goes to model)
103
128
  ├─ no image-capable model available → do nothing (no hint, no persist;
104
129
  │ core's default image handling applies)
105
- └─ text-only main model → persist image to the user-level vision store
106
- (<sha256>.<ext> under <cache>/opencode-vision-analyze/vision;
107
- already-local file paths are read in place, never copied)
130
+ └─ text-only main model → resolve a stable image path:
131
+ path-pasted attachments use the source path in place (never copied,
132
+ always the latest file content); clipboard images are persisted to
133
+ the user-level vision store (<sha256>.<ext> under
134
+ <cache>/opencode-vision-analyze/vision)
108
135
  and inject a synthetic hint (hidden in TUI, visible to model):
109
136
  "use the vision_analyze tool with image_path: ..."
110
137
 
package/README.zh.md CHANGED
@@ -89,9 +89,34 @@ curl 方式说明:
89
89
 
90
90
  ### 存储与缓存
91
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` 条目。
92
+ 两套**用户级共享缓存**并列在 `<cache>/opencode-vision-analyze/` 下,跨会话 / 项目 / 重启共享,与 git 分域无关,无需任何 `.gitignore`。
93
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 进程可安全共享同一描述缓存。
94
+ | 缓存 | 目录 | 内容 | 命名 | 容量上限 |
95
+ |---|---|---|---|---|
96
+ | 图片 | `vision/` | 图片字节 | `<sha256>.<ext>` | 2000 条 / 500MB |
97
+ | 描述 | `descriptions/` | 描述文本(JSON) | `sha256(key)` | 2000 条 / 50MB |
98
+
99
+ **图片存储(`vision/`)**
100
+
101
+ - **写入缓存**:剪贴板位图(无源路径)与 `http(s)` 下载。
102
+ - **原位读用(不复制)**:路径粘贴(消息 part 带真实 `source.path`,如复制到剪贴板的文件路径)与模型直接传入的本地文件路径——分析时当场重读,故同一路径每次粘贴都取最新内容。
103
+ - **淘汰**:按文件 mtime LRU;超出 2000 条或 500MB 即删最久未用的条目。
104
+ - **并发安全**:临时文件 + rename 原子写,多个 opencode 进程可安全共享。
105
+
106
+ **描述缓存(`descriptions/`)**
107
+
108
+ - **key**:`<图片sha256>:<生效问题>`,文件名取 `sha256(key)`。
109
+ - **泛解析(`question` 空 / 省略)**:归一化到固定文案 `Describe this image in full detail, including all text, UI elements, diagrams, or content visible.`,所有泛解析收敛到同一条。
110
+ - **具体追问**:保留原问句,走各自的 `<图片sha256>:<问题>` key(格式与旧版一致,存量条目照常命中)。
111
+ - **写入门槛**:泛解析条目要求描述 ≥ 100 字符,防止视觉模型敷衍/拒答的短文本毒化共享条目。
112
+ - **淘汰**:按文件 mtime LRU,上限 2000 条 / 50MB;**并发安全**同上。
113
+
114
+ **平台默认缓存根**
115
+
116
+ - Linux:`$XDG_CACHE_HOME || ~/.cache`
117
+ - macOS:`~/Library/Caches`(亦接受 `$XDG_CACHE_HOME` 覆盖)
118
+ - Windows:`%LOCALAPPDATA% || ~/AppData/Local`
119
+ - 空串 env 视为未设置,回退默认。
95
120
 
96
121
  ## 工作原理
97
122
 
@@ -102,9 +127,10 @@ curl 方式说明:
102
127
  ├─ 主模型支持图片输入 → 不做任何处理(原图直发)
103
128
  ├─ 无任何 image-capable 模型 → 不做处理(不注入 hint、不落盘;
104
129
  │ 交给核心对图片的默认处理)
105
- └─ 纯文本主模型 → 图片落盘到用户级 vision 存储
106
- (<sha256>.<ext>;位于 <cache>/opencode-vision-analyze/vision;
107
- 本地已存在文件路径原位读用、不复制)
130
+ └─ 纯文本主模型 → 解析出稳定 image_path:
131
+ 路径粘贴用源文件路径原位读用(不复制、始终取最新内容);
132
+ 剪贴板位图落盘到用户级 vision 存储
133
+ (<sha256>.<ext>;位于 <cache>/opencode-vision-analyze/vision)
108
134
  并注入 synthetic 提示(TUI 隐藏、模型可见):
109
135
  "用 vision_analyze 工具查看,image_path: ..."
110
136
 
package/dist/index.js CHANGED
@@ -2,10 +2,12 @@
2
2
  * opencode-vision-analyze
3
3
  *
4
4
  * 为「不具备视觉能力的主模型」提供图片解读路由:当用户在消息中附带图片时,
5
- * 插件把图片落盘到**用户级共享目录**(<cache>/opencode-vision-analyze/vision,
6
- * 恒用户级、与 git/非 git 无关),并向模型注入一条 synthetic 提示
7
- * (TUI 界面隐藏、模型可见),引导它通过 vision_analyze 工具让指定的视觉
8
- * 模型描述图片。若主模型本身支持图片输入,则不做任何干预,原图直接发给主模型。
5
+ * 插件解析出一个稳定的图片绝对路径(**路径粘贴**——part 带真实 source.path——
6
+ * 直接原位读用、不复制;**复制图片本身**(剪贴板位图)等无有效源路径时才落盘到
7
+ * 用户级共享目录 <cache>/opencode-vision-analyze/vision,恒用户级、与 git/非 git
8
+ * 无关),并向模型注入一条 synthetic 提示(TUI 界面隐藏、模型可见),引导它通过
9
+ * vision_analyze 工具让指定的视觉模型描述图片。若主模型本身支持图片输入,则不做
10
+ * 任何干预,原图直接发给主模型。
9
11
  *
10
12
  * 工作方式(vision_analyze 工具路径):主模型调用 vision_analyze 时,插件
11
13
  * 创建一个 parentID 挂在当前会话下的临时子会话(不进会话列表、不生成
@@ -20,8 +22,8 @@
20
22
  * 泛解析条目是全图共享的,写入前按最短文本长度把关,防一条垃圾描述毒化整图。
21
23
  * image_path 除了绝对路径也接受 http(s) URL:先下载落盘到同一用户级 vision
22
24
  * 目录(内容哈希命名,天然与附件落盘去重;LRU + 容量上限,2000 条 / 500MB),
23
- * 再走统一的磁盘加载路径。**本地已存在的文件直接原位读取**(不复制、不 touch),
24
- * 只有贴图/下载才写缓存。
25
+ * 再走统一的磁盘加载路径。**路径粘贴与本地已存在的文件直接原位读取**(不复制、
26
+ * 不 touch,始终读文件最新内容);只有剪贴板贴图/下载才写缓存。
25
27
  * 主模型本身支持图片输入时走快速路径:不做子会话描述,直接把原图作为
26
28
  * 工具附件回传给模型自行查看。
27
29
  *
@@ -752,6 +754,34 @@ const plugin = async (input, optionsArg) => {
752
754
  return undefined;
753
755
  }
754
756
  };
757
+ /**
758
+ * 解析图片 part 的可用绝对路径:
759
+ * - **路径粘贴**(part 带真实 source.path):原位读用,不复制进 vision 缓存。相对路径
760
+ * 以 input.directory 为基准绝对化,与 opencode 工具一致(tool/read.ts 等用
761
+ * path.resolve(instance.directory, filepath))。提示里给绝对路径,vision_analyze
762
+ * 每次调用当场重读该文件 → 始终分析最新内容;描述缓存按内容 sha,天然不缓存旧内容。
763
+ * - **复制图片本身**(剪贴板位图,source.path="clipboard")、无 source、扩展名不受支持、
764
+ * 文件缺失/为空 → 回退 persistImage 内容寻址落盘(贴图必须给模型稳定 image_path)。
765
+ * 判定失败一律回退,绝不抛错(fail-open)。
766
+ */
767
+ const resolveImagePath = async (part) => {
768
+ const src = part.source;
769
+ if (src?.type === "file" && src.path) {
770
+ const abs = path.isAbsolute(src.path) ? src.path : path.resolve(input.directory, src.path);
771
+ const ext = path.extname(abs).toLowerCase();
772
+ if (EXT_MIME[ext]) {
773
+ try {
774
+ const st = await fs.stat(abs);
775
+ if (st.isFile() && st.size > 0)
776
+ return abs;
777
+ }
778
+ catch {
779
+ // 缺失/不可读 → 回退落盘
780
+ }
781
+ }
782
+ }
783
+ return (await persistImage(part))?.filepath;
784
+ };
755
785
  /**
756
786
  * chat.message 钩子:用户消息持久化前触发(parts 数组与持久化同引用,
757
787
  * push 进去的 part 会一并入库)。
@@ -762,7 +792,8 @@ const plugin = async (input, optionsArg) => {
762
792
  * 3. 递归防护——消息模型 ∈ 候选链全体成员(我们的描述子会话)则放行;
763
793
  * 4. 能力门控——主模型本身能看图则不注入提示;
764
794
  * 5. 空链降级——没有任何可用视觉模型时不注入 hint;
765
- * 6. 图片落盘,并注入一条 synthetic text part 引导模型使用 vision_analyze。
795
+ * 6. 解析图片路径(路径粘贴原位读用、其余落盘),并注入一条 synthetic text part
796
+ * 引导模型使用 vision_analyze。
766
797
  */
767
798
  const onChatMessage = async (hookInput, output) => {
768
799
  // 记录会话当前模型,供后续 vision_analyze 快速路径与未显式指定 model 的
@@ -789,14 +820,14 @@ const plugin = async (input, optionsArg) => {
789
820
  const chain = await resolveChain();
790
821
  if (chain.length === 0)
791
822
  return;
792
- // 每张图落盘并生成两行提示;任何一张落盘失败就跳过该图(不影响其余图片)。
823
+ // 每张图解析路径并生成两行提示;任一张解析失败就跳过该图(不影响其余图片)。
793
824
  const lines = [];
794
825
  for (const part of images) {
795
- const persisted = await persistImage(part);
796
- if (!persisted)
826
+ const imagePath = await resolveImagePath(part);
827
+ if (!imagePath)
797
828
  continue;
798
829
  lines.push(`[The user attached an image: ${part.filename ?? "image"}]`);
799
- lines.push(`[Examine it with the vision_analyze tool using image_path: ${persisted.filepath}]`);
830
+ lines.push(`[Examine it with the vision_analyze tool using image_path: ${imagePath}]`);
800
831
  }
801
832
  if (lines.length === 0)
802
833
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-vision-analyze",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",