@xiaoqiong0v0/opencode-file-tool 1.0.5 → 1.0.8
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 +8 -2
- package/dist/file-tool.js +85 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,9 +14,10 @@ OpenCode 文件缓存与图片分析插件。自动缓存用户粘贴的图片
|
|
|
14
14
|
|
|
15
15
|
## 功能
|
|
16
16
|
|
|
17
|
-
- **文件缓存** — 粘贴图片时自动缓存到 `~/.opencode/plugins-cache/`
|
|
17
|
+
- **文件缓存** — 粘贴图片时自动缓存到 `~/.opencode/plugins-cache/file-tool/`
|
|
18
18
|
- **图片分析** — 通过 `analyze_image file_id:N` 调用视觉模型分析
|
|
19
19
|
- **会话父子链** — 子 agent 会话可通过 parent 链回退读取祖先会话的缓存
|
|
20
|
+
- **开关控制** — 主模型支持视觉时可用 `disable` 关闭缓存,让模型直接读取图片
|
|
20
21
|
- **多语言** — 支持中/英文提示
|
|
21
22
|
|
|
22
23
|
## 工具
|
|
@@ -26,6 +27,11 @@ OpenCode 文件缓存与图片分析插件。自动缓存用户粘贴的图片
|
|
|
26
27
|
| `file_tool list-provider` | 列出可用模型 |
|
|
27
28
|
| `file_tool set-provider <model>` | 切换视觉模型 |
|
|
28
29
|
| `file_tool list-cache [all\|N\|main\|main N]` | 查看缓存文件 |
|
|
30
|
+
| `file_tool enable` | 临时开启图片缓存(重启恢复默认) |
|
|
31
|
+
| `file_tool disable` | 临时关闭图片缓存(重启恢复默认) |
|
|
32
|
+
| `file_tool enable-save` | 持久开启图片缓存(写入配置) |
|
|
33
|
+
| `file_tool disable-save` | 持久关闭图片缓存(写入配置) |
|
|
34
|
+
| `file_tool status` | 查看缓存开关状态和当前视觉模型 |
|
|
29
35
|
| `analyze_image file_id:N` | 分析指定图片 |
|
|
30
36
|
|
|
31
37
|
## 命令
|
|
@@ -36,7 +42,7 @@ OpenCode 文件缓存与图片分析插件。自动缓存用户粘贴的图片
|
|
|
36
42
|
|
|
37
43
|
## 配置
|
|
38
44
|
|
|
39
|
-
`~/.config/opencode/file-tool.jsonc`
|
|
45
|
+
`~/.config/opencode/file-tool.jsonc` 在首次启动时自动生成,支持 `enabled` 字段控制缓存开关。
|
|
40
46
|
|
|
41
47
|
## GitHub
|
|
42
48
|
|
package/dist/file-tool.js
CHANGED
|
@@ -18,11 +18,14 @@ const FILE_TOOL_CFG_SAMPLE = `{
|
|
|
18
18
|
// 缓存消息数量上限,超过则删除最早的
|
|
19
19
|
"maxCacheMessages": 3,
|
|
20
20
|
// 工具提示语言:zh=中文, en=English
|
|
21
|
-
"lang": "en"
|
|
21
|
+
"lang": "en",
|
|
22
|
+
// 是否启用图片缓存
|
|
23
|
+
"enabled": true
|
|
22
24
|
}
|
|
23
25
|
`;
|
|
24
26
|
let MAX_CACHE_MSGS = 3;
|
|
25
27
|
let LANG = "en";
|
|
28
|
+
let ENABLED = true;
|
|
26
29
|
const TX = {
|
|
27
30
|
file_not_found: { zh: "文件不存在: {path}", en: "File not found: {path}" },
|
|
28
31
|
file_id_not_found: { zh: "文件ID不存在: {id}", en: "File ID not found: {id}" },
|
|
@@ -34,7 +37,7 @@ const TX = {
|
|
|
34
37
|
model_not_set: { zh: "未设置", en: "not set" },
|
|
35
38
|
model_switched: { zh: "视觉模型已切换为: {model}", en: "Vision model set to: {model}" },
|
|
36
39
|
specify_model: { zh: "请指定模型名", en: "Specify a model name" },
|
|
37
|
-
unknown_cmd: { zh: "未知命令: {cmd}\n可用: list-provider, set-provider <model>, list-cache [all|N|main|main N]", en: "Unknown command: {cmd}\nAvailable: list-provider, set-provider <model>, list-cache [all|N|main|main N]" },
|
|
40
|
+
unknown_cmd: { zh: "未知命令: {cmd}\n可用: list-provider, set-provider <model>, list-cache [all|N|main|main N], enable, disable, enable-save, disable-save, status", en: "Unknown command: {cmd}\nAvailable: list-provider, set-provider <model>, list-cache [all|N|main|main N], enable, disable, enable-save, disable-save, status" },
|
|
38
41
|
config_error: { zh: "请在 file-tool.jsonc 中配置 model (provider/modelId) 或 apiKey+apiBaseUrl+model", en: "Set model (provider/modelId) or apiKey+apiBaseUrl+model in file-tool.jsonc" },
|
|
39
42
|
meta_failed: { zh: "分析失败", en: "Failed" },
|
|
40
43
|
meta_skip: { zh: "跳过", en: "Skip" },
|
|
@@ -42,12 +45,18 @@ const TX = {
|
|
|
42
45
|
meta_image: { zh: "图片", en: "Image" },
|
|
43
46
|
meta_error: { zh: "分析出错", en: "Error" },
|
|
44
47
|
no_cache: { zh: "[] (无缓存)", en: "[] (no cache)" },
|
|
48
|
+
enabled: { zh: "已启用", en: "Enabled" },
|
|
49
|
+
disabled: { zh: "已禁用", en: "Disabled" },
|
|
50
|
+
status: { zh: "图片缓存: {s}\n视觉模型: {m}", en: "Image cache: {s}\nVision model: {m}" },
|
|
51
|
+
status_cmd: { zh: "查看缓存开关状态", en: "Show cache status" },
|
|
45
52
|
vision_prompt_default: { zh: "请详细描述这张图片的内容,返回格式: [文件名] 描述", en: "Describe this image in detail, format: [filename] description" },
|
|
46
53
|
err_resolve_config: { zh: "无法解析模型配置: {model}。请在 file-tool.jsonc 中配置 model (provider/modelId) 或 apiKey+apiBaseUrl+model", en: "Cannot resolve model config: {model}. Set model (provider/modelId) or apiKey+apiBaseUrl+model in file-tool.jsonc" },
|
|
47
54
|
err_api: { zh: "API {status}: {msg}", en: "API {status}: {msg}" },
|
|
48
55
|
empty_response: { zh: "(空)", en: "(empty)" },
|
|
56
|
+
uncached: { zh: "未缓存", en: "uncached" },
|
|
57
|
+
uncached_hint: { zh: "文件未缓存(id={id}),请先启用缓存再操作", en: "File not cached (id={id}), enable cache first" },
|
|
49
58
|
cmd_desc: { zh: "切换视觉分析模型", en: "Switch vision analysis model" },
|
|
50
|
-
cmd_template: { zh: "直接调用 file_tool
|
|
59
|
+
cmd_template: { zh: "直接调用 file_tool 工具。`list-provider` 列出模型,`set-provider <模型名>` 切换模型,`list-cache` 查看缓存,`enable/disable` 临时开关,`enable-save/disable-save` 持久化开关,`status` 查看状态。", en: "Call file_tool tool directly. `list-provider` list models, `set-provider <model>` switch, `list-cache` view cache, `enable/disable` temp toggle, `enable-save/disable-save` persist toggle, `status` show state." },
|
|
51
60
|
};
|
|
52
61
|
const T = (key, params) => {
|
|
53
62
|
const entry = TX[key] || { zh: key, en: key };
|
|
@@ -67,9 +76,16 @@ function loadCfg() {
|
|
|
67
76
|
_cfg = resolveConfig(raw);
|
|
68
77
|
MAX_CACHE_MSGS = (raw.maxCacheMessages > 0) ? raw.maxCacheMessages : 3;
|
|
69
78
|
LANG = (raw.lang === "zh" ? "zh" : "en");
|
|
79
|
+
ENABLED = raw.enabled !== false;
|
|
70
80
|
return _cfg;
|
|
71
81
|
}
|
|
72
82
|
function reloadCfg() { loadCfg(); }
|
|
83
|
+
function writeCfgField(key, value) {
|
|
84
|
+
const cfg = existsSync(CONFIG_PATH) ? readJsonc(CONFIG_PATH) : {};
|
|
85
|
+
cfg[key] = value;
|
|
86
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
|
87
|
+
reloadCfg();
|
|
88
|
+
}
|
|
73
89
|
try {
|
|
74
90
|
loadCfg();
|
|
75
91
|
}
|
|
@@ -82,12 +98,12 @@ const DESC = {
|
|
|
82
98
|
en: "Analyze images with multimodal model. Call file_tool list-cache first to get file IDs, then use file_id:N.",
|
|
83
99
|
},
|
|
84
100
|
file_tool: {
|
|
85
|
-
zh: "文件缓存管理。当你在上下文中看到 [Image N] 或收到 Cannot read 图片错误时,立即调 list-cache 获取文件ID,再用 analyze_image file_id:N
|
|
86
|
-
en: "File cache manager. When you see [Image N] or a Cannot read image error, call list-cache to get file IDs, then use analyze_image file_id:N.",
|
|
101
|
+
zh: "文件缓存管理。当你在上下文中看到 [Image N] 或收到 Cannot read 图片错误时,立即调 list-cache 获取文件ID,再用 analyze_image file_id:N 分析。主模型能直接读取图片时建议用 `disable` 关闭缓存。",
|
|
102
|
+
en: "File cache manager. When you see [Image N] or a Cannot read image error, call list-cache to get file IDs, then use analyze_image file_id:N. If the main model can read images directly, use `disable` to turn off caching.",
|
|
87
103
|
},
|
|
88
104
|
file_tool_args: {
|
|
89
|
-
zh: "list-cache
|
|
90
|
-
en: "list-cache
|
|
105
|
+
zh: "list-cache [all|N|main|main N], list-provider, set-provider <model>, enable/disable(临时), enable-save/disable-save(持久化), status — list-cache 查看缓存(不参数=最近1条,all=全部,N=最近N条,main=主会话,main N=主会话最近N条)",
|
|
106
|
+
en: "list-cache [all|N|main|main N], list-provider, set-provider <model>, enable/disable (temp), enable-save/disable-save (persist), status — list-cache: no arg=last 1, all=all, N=last N, main=root, main N=last N from root",
|
|
91
107
|
},
|
|
92
108
|
analyze_args_source: { zh: "file_path=file_id:N", en: "file_path=file_id:N" },
|
|
93
109
|
analyze_args_data: { zh: "file_id:N 或 base64", en: "file_id:N or base64" },
|
|
@@ -215,6 +231,22 @@ function deleteSession(sid) {
|
|
|
215
231
|
if (existsSync(dir))
|
|
216
232
|
rmSync(dir, { recursive: true, force: true });
|
|
217
233
|
}
|
|
234
|
+
function removeMsgCache(sid, msgId) {
|
|
235
|
+
const data = readSession(sid);
|
|
236
|
+
const idx = data.messages.findIndex(m => m.msgId === msgId);
|
|
237
|
+
if (idx < 0) {
|
|
238
|
+
log.info(`${sid}: msg ${msgId.slice(-8)} not in cache, skip`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const [msg] = data.messages.splice(idx, 1);
|
|
242
|
+
for (const fid of msg.fileIds) {
|
|
243
|
+
delete data.files[fid];
|
|
244
|
+
const path = join(filesDir(sid), fid + ".b64");
|
|
245
|
+
rm(path, { force: true }).catch(() => { });
|
|
246
|
+
}
|
|
247
|
+
log.info(`${sid}: removed msg ${msgId.slice(-8)} (${msg.fileIds.length} files: ${msg.fileIds.join(", ")})`);
|
|
248
|
+
writeSession(sid, data);
|
|
249
|
+
}
|
|
218
250
|
// V1 export:工具 + 事件
|
|
219
251
|
export const FileTool = async () => {
|
|
220
252
|
log.loaded();
|
|
@@ -249,14 +281,17 @@ export const FileTool = async () => {
|
|
|
249
281
|
const part = props?.part;
|
|
250
282
|
if (part?.type === "file" && (part?.mime || "").startsWith("image/")) {
|
|
251
283
|
const fn = (part.filename || part.name || "");
|
|
252
|
-
if (fn) {
|
|
253
|
-
const data = readSession(sid
|
|
254
|
-
if (!sid)
|
|
255
|
-
return;
|
|
284
|
+
if (fn && sid) {
|
|
285
|
+
const data = readSession(sid);
|
|
256
286
|
const fid = data.nextId++;
|
|
257
287
|
const msgId = (part.messageID || "");
|
|
258
|
-
data.files[fid] = { id: fid, filename: fn, mime: part.mime, msgId };
|
|
259
|
-
|
|
288
|
+
data.files[fid] = { id: fid, filename: fn, mime: part.mime, msgId, cached: ENABLED };
|
|
289
|
+
if (ENABLED) {
|
|
290
|
+
writeFileData(sid, fid, (part.url || ""));
|
|
291
|
+
log.info(`${sid}: cached ${fn} as #${fid}`);
|
|
292
|
+
}
|
|
293
|
+
else
|
|
294
|
+
log.info(`${sid}: skip cache ${fn} (disabled)`);
|
|
260
295
|
const msgs = data.messages;
|
|
261
296
|
const last = msgs[msgs.length - 1];
|
|
262
297
|
if (last && last.msgId === msgId) {
|
|
@@ -269,6 +304,18 @@ export const FileTool = async () => {
|
|
|
269
304
|
}
|
|
270
305
|
}
|
|
271
306
|
}
|
|
307
|
+
if (event.type === "message.removed" && sid) {
|
|
308
|
+
const msgId = props?.messageID || "";
|
|
309
|
+
log.info(`${sid}: message.removed msgId=${msgId.slice(-8) || "(empty)"}`);
|
|
310
|
+
if (msgId)
|
|
311
|
+
removeMsgCache(sid, msgId);
|
|
312
|
+
}
|
|
313
|
+
if (event.type === "message.part.removed" && sid) {
|
|
314
|
+
const msgId = props?.messageID || props?.info?.id || "";
|
|
315
|
+
log.info(`${sid}: message.part.removed msgId=${msgId.slice(-8) || "(empty)"}`);
|
|
316
|
+
if (msgId)
|
|
317
|
+
removeMsgCache(sid, msgId);
|
|
318
|
+
}
|
|
272
319
|
},
|
|
273
320
|
tool: {
|
|
274
321
|
analyze_image: tool({
|
|
@@ -288,6 +335,10 @@ export const FileTool = async () => {
|
|
|
288
335
|
return T("file_id_not_found", { id: String(fid) });
|
|
289
336
|
}
|
|
290
337
|
const file = found.file;
|
|
338
|
+
if (!file.cached) {
|
|
339
|
+
context.metadata?.({ title: T("meta_skip") });
|
|
340
|
+
return T("uncached_hint", { id: String(fid) });
|
|
341
|
+
}
|
|
291
342
|
if (!file.mime.startsWith("image/")) {
|
|
292
343
|
context.metadata?.({ title: T("meta_skip") });
|
|
293
344
|
return T("not_an_image", { name: file.filename, mime: file.mime });
|
|
@@ -363,6 +414,26 @@ export const FileTool = async () => {
|
|
|
363
414
|
reloadCfg();
|
|
364
415
|
return T("model_switched", { model });
|
|
365
416
|
}
|
|
417
|
+
if (cmd === "disable") {
|
|
418
|
+
ENABLED = false;
|
|
419
|
+
return T("disabled");
|
|
420
|
+
}
|
|
421
|
+
if (cmd === "enable") {
|
|
422
|
+
ENABLED = true;
|
|
423
|
+
return T("enabled");
|
|
424
|
+
}
|
|
425
|
+
if (cmd === "disable-save") {
|
|
426
|
+
writeCfgField("enabled", false);
|
|
427
|
+
return T("disabled");
|
|
428
|
+
}
|
|
429
|
+
if (cmd === "enable-save") {
|
|
430
|
+
writeCfgField("enabled", true);
|
|
431
|
+
return T("enabled");
|
|
432
|
+
}
|
|
433
|
+
if (cmd === "status") {
|
|
434
|
+
const cfg = existsSync(CONFIG_PATH) ? readJsonc(CONFIG_PATH) : {};
|
|
435
|
+
return T("status", { s: ENABLED ? T("enabled") : T("disabled"), m: cfg.model || T("model_not_set") });
|
|
436
|
+
}
|
|
366
437
|
if (cmd === "list-cache" || cmd.startsWith("list-cache ")) {
|
|
367
438
|
const arg = cmd === "list-cache" ? "1" : cmd.slice(11).trim();
|
|
368
439
|
let targetSid = context.sessionID;
|
|
@@ -392,7 +463,7 @@ export const FileTool = async () => {
|
|
|
392
463
|
for (const fid of msg.fileIds) {
|
|
393
464
|
const f = data.files[fid];
|
|
394
465
|
if (f)
|
|
395
|
-
out += ` ${f.filename}: ${f.id}\n`;
|
|
466
|
+
out += ` ${f.filename}: ${f.id}${f.cached ? "" : ` (${T("uncached")})`}\n`;
|
|
396
467
|
}
|
|
397
468
|
}
|
|
398
469
|
return out.trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaoqiong0v0/opencode-file-tool",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "File cache & image analysis plugin for OpenCode. Auto-caches pasted images, analyzes via multimodal model.",
|
|
6
6
|
"files": [
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
".": "./dist/file-tool.js"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "tsc
|
|
15
|
+
"build": "tsc",
|
|
16
16
|
"prepublishOnly": "npm run build"
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|