@xiaoqiong0v0/opencode-file-tool 1.1.1 → 1.2.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/dist/file-tool.js CHANGED
@@ -1,9 +1,6 @@
1
- import { T } from "./i18n.js";
2
1
  import { log, ENABLED, loadCfg } from "./config.js";
3
2
  import { registerInputFile, migrateLegacyCache, knownSessions, sessionParents, deleteSession, removeMsgCache, extForMime } from "./cache.js";
4
- import { analyzeImageTool } from "./tools/analyze.js";
5
- import { textToImageTool, textToVideoTool, textToSpeechTool } from "./tools/generate.js";
6
- import { fileTool } from "./tools/file_tool.js";
3
+ import { fileToolCli } from "./tools/cli.js";
7
4
  try {
8
5
  loadCfg();
9
6
  }
@@ -13,11 +10,6 @@ catch (e) {
13
10
  export const fileToolPlugin = async () => {
14
11
  log.loaded();
15
12
  return {
16
- config: async (config) => {
17
- const commands = config.command ?? {};
18
- commands["file-tool"] = { template: T("cmd_template"), description: T("cmd_desc") };
19
- config.command = commands;
20
- },
21
13
  event: async ({ event }) => {
22
14
  const props = event.properties;
23
15
  const sid = props?.sessionID;
@@ -71,11 +63,7 @@ export const fileToolPlugin = async () => {
71
63
  }
72
64
  },
73
65
  tool: {
74
- analyze_image: analyzeImageTool,
75
- text_to_image: textToImageTool,
76
- text_to_video: textToVideoTool,
77
- text_to_speech: textToSpeechTool,
78
- file_tool: fileTool,
66
+ file_tool: fileToolCli,
79
67
  },
80
68
  };
81
69
  };
package/dist/i18n.js CHANGED
@@ -40,32 +40,10 @@ export const TX = {
40
40
  empty_response: { zh: "(空)", en: "(empty)" },
41
41
  uncached: { zh: "未缓存", en: "uncached" },
42
42
  uncached_hint: { zh: "文件未缓存(id={id}),请先启用缓存再操作", en: "File not cached (id={id}), enable cache first" },
43
- cmd_desc: { zh: "文件缓存管理 + 多模型配置(视觉/文生图/文生视频/文生语音)", en: "File cache manager + multi-model config (vision/image/video/tts)" },
44
- cmd_template: { zh: "直接调用 file_tool。`list-cache input` 看用户图片,`list-cache image|video|tts` 看生成产物,`list-provider` 列出模型,`set-provider [类型:]模型名` 切换,`enable/disable` 临时开关,`enable-save/disable-save` 持久化,`status` 查看状态。", en: "Call file_tool. `list-cache input` user images, `list-cache image|video|tts` generated, `list-provider` list models, `set-provider [type:]model` switch, `enable/disable` temp, `enable-save/disable-save` persist, `status` show state." },
45
43
  };
46
44
  export const DESC = {
47
- analyze_image: {
48
- zh: "用多模态模型分析图片。file_id:类型:id,类型: input=用户图片/image=生成图。先用 list-cache ID 再分析。",
49
- en: "Analyze image with multimodal model. file_id:type:id; types: input=user image/image=generated. Get ID via list-cache first.",
50
- },
51
- text_to_image: { zh: "文生图:根据文本提示生成图片,结果缓存并返回 file_id:类型:id,可用 analyze_image 查看。", en: "Text-to-image: generate an image from a prompt, cached and returned as file_id:type:id." },
52
- text_to_video: { zh: "文生视频:根据文本提示生成视频(异步提交+轮询),结果缓存并返回 file_id:类型:id。", en: "Text-to-video: generate a video from a prompt (async submit+poll), cached and returned as file_id:type:id." },
53
- text_to_speech: { zh: "文生语音:将文本转为语音(TTS),结果缓存并返回 file_id:类型:id。", en: "Text-to-speech: convert text to audio, cached and returned as file_id:type:id." },
54
- file_tool: {
55
- zh: "文件缓存管理。类型: input=用户图片, image/video/tts=生成产物。查看用户图片用 `list-cache input`;生成产物用 `list-cache image|video|tts`。file_id:类型:id 供 analyze_image 分析。",
56
- en: "File cache. Types: input=user images, image/video/tts=generated. View user images via `list-cache input`; generated via `list-cache image|video|tts`. file_id:type:id for analyze_image.",
57
- },
58
- file_tool_args: {
59
- zh: "list-cache [类型] [数量]: input=用户图片(最常用)/image/video/tts=生成产物; list-provider; set-provider [类型:]模型; enable/disable; enable-save/disable-save; status; main 前缀查主会话",
60
- en: "list-cache [type] [count]: input=user images(common)/image/video/tts=generated; list-provider; set-provider [type:]model; enable/disable; enable-save/disable-save; status; main prefix for root session",
61
- },
62
- analyze_args_source: { zh: "file_path=file_id:类型:id(如 file_id:image:2)", en: "file_path=file_id:type:id (e.g. file_id:image:2)" },
63
- analyze_args_data: { zh: "file_id:类型:id 或 base64", en: "file_id:type:id or base64" },
64
- analyze_args_prompt: { zh: "分析提示", en: "prompt" },
65
- gen_args_prompt: { zh: "生成提示词", en: "generation prompt" },
66
- gen_args_size: { zh: "图片尺寸(如 1024x1024)", en: "image size (e.g. 1024x1024)" },
67
- gen_args_duration: { zh: "视频时长(秒)", en: "video duration (seconds)" },
68
- gen_args_voice: { zh: "音色(如 alloy)", en: "voice (e.g. alloy)" },
45
+ file_tool: { zh: "统一命令行工具。子命令: analyze 分析图 / imagine 文生图 / video 文生视频 / tts 文生语音 / list 缓存列表 / providers 模型列表 / set 切换模型 / status / enable-disable。空参数或 help 看用法。", en: "Single CLI tool. Subcommands: analyze image / imagine t2i / video t2v / tts / list cache / providers models / set model / status / enable-disable. Empty or help for usage." },
46
+ file_tool_args: { zh: "完整命令行字符串,如 'list input'、'imagine 一只橘猫'、'analyze file_id:input:1';空时默认 help。类型: input=用户图片, image/video/tts=生成产物。", en: "Full command string, e.g. 'list input', 'imagine a cat', 'analyze file_id:input:1'; empty defaults to help. Types: input=user image, image/video/tts=generated." },
69
47
  };
70
48
  export function T(key, params) {
71
49
  const entry = TX[key] || { zh: key, en: key };
@@ -0,0 +1,342 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import stringArgv from "string-argv";
5
+ import { parseArgs } from "node:util";
6
+ import { T, LANG, DESC } from "../i18n.js";
7
+ import { CONFIG_PATH, MODEL_TYPES, CACHE_TYPES, DEFAULT_CFG, getCfg, getProviderNames, readJsonc, ENABLED, setEnabled, saveCfg, requireModelCfg } from "../config.js";
8
+ import { readTypeStore, getRootSession, findFileInChain, readFileData, registerGeneratedFile, fetchToBuffer, buildGenFilename, extForMime } from "../cache.js";
9
+ import { generateVideo } from "../providers.js";
10
+ import { genError } from "../utils.js";
11
+ import { localModelList, listModelsApi, currentModelSummary } from "./file_tool_helpers.js";
12
+ const GEN_TIMEOUT = 300000;
13
+ function helpText() {
14
+ if (LANG === "zh") {
15
+ return `用法: file_tool <子命令> [参数]
16
+ 子命令:
17
+ analyze <file_id:类型:id|路径|base64> [提示语] 分析图片
18
+ imagine <提示词> [--size 1024x1024] 文生图
19
+ video <提示词> [--duration 5] 文生视频(异步)
20
+ tts <文本> [--voice alloy] 文生语音
21
+ list [类型] [数量] 缓存列表 (input=用户图/image/video/tts=生成)
22
+ providers 列出可用模型
23
+ set [类型:]模型名 切换模型
24
+ status 查看状态
25
+ enable|disable|enable-save|disable-save 缓存开关
26
+ help 此帮助
27
+ 示例:
28
+ file_tool analyze file_id:input:1
29
+ file_tool imagine "一只橘猫" --size 512x512
30
+ file_tool list input
31
+ file_tool set image:agnes/agnes-image-2.1-flash`;
32
+ }
33
+ return `Usage: file_tool <subcommand> [args]
34
+ Subcommands:
35
+ analyze <file_id:type:id|path|base64> [prompt] analyze image
36
+ imagine <prompt> [--size 1024x1024] text-to-image
37
+ video <prompt> [--duration 5] text-to-video (async)
38
+ tts <text> [--voice alloy] text-to-speech
39
+ list [type] [count] list cache (input=user image/image/video/tts=generated)
40
+ providers list available models
41
+ set [type:]model switch model
42
+ status show state
43
+ enable|disable|enable-save|disable-save cache toggle
44
+ help this help
45
+ Examples:
46
+ file_tool analyze file_id:input:1
47
+ file_tool imagine "an orange cat" --size 512x512
48
+ file_tool list input
49
+ file_tool set image:agnes/agnes-image-2.1-flash`;
50
+ }
51
+ async function analyzeCmd(args, context) {
52
+ const { positionals } = parseArgs({ args, allowPositionals: true });
53
+ let data = positionals[0] || "";
54
+ let prompt = positionals.slice(1).join(" ") || "";
55
+ if (!data)
56
+ return `${T("specify_model")}\n\n${helpText()}`;
57
+ let imageUrl, fileName = "";
58
+ if (data.startsWith("file_id:")) {
59
+ const segs = data.slice(8).split(":");
60
+ let type = "input";
61
+ let fid;
62
+ if (segs.length === 2 && CACHE_TYPES.includes(segs[0])) {
63
+ type = segs[0];
64
+ fid = parseInt(segs[1], 10);
65
+ }
66
+ else {
67
+ fid = parseInt(segs[0], 10);
68
+ }
69
+ const found = findFileInChain(context.sessionID, type, fid);
70
+ if (!found) {
71
+ context.metadata?.({ title: T("meta_failed") });
72
+ return T("file_id_not_found", { id: `${type}:${fid}` });
73
+ }
74
+ const file = found.file;
75
+ if (!file.cached) {
76
+ context.metadata?.({ title: T("meta_skip") });
77
+ return T("uncached_hint", { id: `${type}:${fid}` });
78
+ }
79
+ if (!file.mime.startsWith("image/")) {
80
+ context.metadata?.({ title: T("meta_skip") });
81
+ return T("not_an_image", { name: file.filename, mime: file.mime });
82
+ }
83
+ fileName = file.filename;
84
+ imageUrl = readFileData(context.sessionID, type, fid) || "";
85
+ if (!imageUrl) {
86
+ context.metadata?.({ title: T("meta_failed") });
87
+ return T("file_data_not_found", { id: `${type}:${fid}` });
88
+ }
89
+ prompt = prompt || T("describe_image", { name: fileName });
90
+ }
91
+ else if (existsSync(data) || existsSync(join(context.directory, data))) {
92
+ if (!existsSync(data))
93
+ data = join(context.directory, data);
94
+ if (!existsSync(data)) {
95
+ context.metadata?.({ title: T("meta_not_found") });
96
+ return T("file_not_found", { path: data });
97
+ }
98
+ const ext = data.split(".").pop()?.toLowerCase() || "";
99
+ const mimeMap = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", bmp: "image/bmp", gif: "image/gif", webp: "image/webp" };
100
+ const mime = mimeMap[ext] || "image/png";
101
+ fileName = data.split(/[/\\]/).pop() || "";
102
+ imageUrl = `data:${mime};base64,${readFileSync(data).toString("base64")}`;
103
+ }
104
+ else {
105
+ imageUrl = `data:image/png;base64,${data.replace(/^data:image\/\w+;base64,/, "")}`;
106
+ }
107
+ try {
108
+ const cfg = requireModelCfg("vision");
109
+ const resp = await fetch(`${cfg.baseURL}/chat/completions`, {
110
+ method: "POST",
111
+ headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json" },
112
+ body: JSON.stringify({ model: cfg.modelId, messages: [{ role: "user", content: [{ type: "text", text: prompt || T("vision_prompt_default") }, { type: "image_url", image_url: { url: imageUrl } }] }], max_tokens: getCfg().maxTokens }),
113
+ signal: AbortSignal.timeout(getCfg().timeout || 60000),
114
+ });
115
+ if (!resp.ok)
116
+ throw new Error(T("err_api", { status: String(resp.status), msg: (await resp.text().catch(() => "unknown")).slice(0, 200) }));
117
+ const d = await resp.json();
118
+ const msg = d.choices?.[0]?.message;
119
+ const result = msg?.content || msg?.reasoning_content || T("empty_response");
120
+ context.metadata?.({ title: `[Vision] ${fileName || T("meta_image")}`, metadata: { sessionID: context.sessionID, messageID: context.messageID } });
121
+ return "[Vision] " + result;
122
+ }
123
+ catch (e) {
124
+ const msg = e instanceof Error ? e.message : String(e);
125
+ context.metadata?.({ title: T("meta_error") });
126
+ return `[Vision Error] ${msg}`;
127
+ }
128
+ }
129
+ async function imagineCmd(args, context) {
130
+ try {
131
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { size: { type: "string" } } });
132
+ const prompt = positionals.join(" ");
133
+ const size = values.size;
134
+ const cfg = requireModelCfg("image");
135
+ const body = { model: cfg.modelId, prompt };
136
+ if (size)
137
+ body.size = size;
138
+ const resp = await fetch(`${cfg.baseURL}/images/generations`, {
139
+ method: "POST",
140
+ headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json" },
141
+ body: JSON.stringify(body),
142
+ signal: AbortSignal.timeout(GEN_TIMEOUT),
143
+ });
144
+ if (!resp.ok)
145
+ throw new Error(T("err_api", { status: String(resp.status), msg: (await resp.text().catch(() => "unknown")).slice(0, 200) }));
146
+ const data = await resp.json();
147
+ const item = data?.data?.[0];
148
+ if (!item)
149
+ throw new Error(T("gen_no_result"));
150
+ let buffer, mime = "image/png";
151
+ if (item.b64_json)
152
+ buffer = Buffer.from(item.b64_json, "base64");
153
+ else if (item.url) {
154
+ const d = await fetchToBuffer(item.url);
155
+ buffer = d.buffer;
156
+ mime = d.mime;
157
+ }
158
+ else
159
+ throw new Error(T("gen_no_result"));
160
+ const store = readTypeStore(context.sessionID, "image");
161
+ const r = registerGeneratedFile(context.sessionID, "image", buildGenFilename("image", mime, store.nextId), mime, context.messageID, buffer);
162
+ context.metadata?.({ title: "[ImageGen]", metadata: { sessionID: context.sessionID, messageID: context.messageID } });
163
+ return T("image_generated", { name: `image_${r.id}.${extForMime(mime)}`, id: `image:${r.id}`, path: r.path });
164
+ }
165
+ catch (e) {
166
+ context.metadata?.({ title: T("meta_error") });
167
+ return genError(e);
168
+ }
169
+ }
170
+ async function videoCmd(args, context) {
171
+ try {
172
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { duration: { type: "string" } } });
173
+ const prompt = positionals.join(" ");
174
+ const duration = values.duration ? parseInt(values.duration, 10) : undefined;
175
+ const cfg = requireModelCfg("video");
176
+ const url = await generateVideo(cfg, prompt, duration);
177
+ const d = await fetchToBuffer(url);
178
+ const store = readTypeStore(context.sessionID, "video");
179
+ const r = registerGeneratedFile(context.sessionID, "video", buildGenFilename("video", d.mime, store.nextId), d.mime, context.messageID, d.buffer);
180
+ context.metadata?.({ title: "[VideoGen]", metadata: { sessionID: context.sessionID, messageID: context.messageID } });
181
+ return T("video_generated", { name: `video_${r.id}.${extForMime(d.mime)}`, id: `video:${r.id}`, path: r.path });
182
+ }
183
+ catch (e) {
184
+ context.metadata?.({ title: T("meta_error") });
185
+ return genError(e);
186
+ }
187
+ }
188
+ async function ttsCmd(args, context) {
189
+ try {
190
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { voice: { type: "string" } } });
191
+ const text = positionals.join(" ");
192
+ const voice = values.voice;
193
+ const cfg = requireModelCfg("tts");
194
+ const body = { model: cfg.modelId, input: text };
195
+ if (voice)
196
+ body.voice = voice;
197
+ const resp = await fetch(`${cfg.baseURL}/audio/speech`, {
198
+ method: "POST",
199
+ headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json" },
200
+ body: JSON.stringify(body),
201
+ signal: AbortSignal.timeout(GEN_TIMEOUT),
202
+ });
203
+ if (!resp.ok)
204
+ throw new Error(T("err_api", { status: String(resp.status), msg: (await resp.text().catch(() => "unknown")).slice(0, 200) }));
205
+ const buf = Buffer.from(await resp.arrayBuffer());
206
+ const mime = resp.headers.get("content-type")?.split(";")[0] || "audio/mpeg";
207
+ const store = readTypeStore(context.sessionID, "tts");
208
+ const r = registerGeneratedFile(context.sessionID, "tts", buildGenFilename("tts", mime, store.nextId), mime, context.messageID, buf);
209
+ context.metadata?.({ title: "[TTS]", metadata: { sessionID: context.sessionID, messageID: context.messageID } });
210
+ return T("tts_generated", { name: `tts_${r.id}.${extForMime(mime)}`, id: `tts:${r.id}`, path: r.path });
211
+ }
212
+ catch (e) {
213
+ context.metadata?.({ title: T("meta_error") });
214
+ return genError(e);
215
+ }
216
+ }
217
+ async function providersCmd() {
218
+ const providerNames = getProviderNames();
219
+ const models = await listModelsApi(providerNames);
220
+ const fallbackModels = localModelList();
221
+ for (const m of fallbackModels)
222
+ if (!models.includes(m))
223
+ models.push(m);
224
+ const modelLines = models.map(m => " " + m).join("\n");
225
+ return T("current_model", { model: currentModelSummary(), list: modelLines });
226
+ }
227
+ function setCmd(args) {
228
+ const { positionals } = parseArgs({ args, allowPositionals: true });
229
+ const arg = positionals.join(":") || positionals[0] || "";
230
+ if (!arg)
231
+ return T("specify_model");
232
+ let type = "vision";
233
+ let model = arg;
234
+ const colon = arg.indexOf(":");
235
+ if (colon > 0 && MODEL_TYPES.includes(arg.slice(0, colon))) {
236
+ type = arg.slice(0, colon);
237
+ model = arg.slice(colon + 1).trim();
238
+ }
239
+ if (!model)
240
+ return T("specify_model");
241
+ const cfg = existsSync(CONFIG_PATH) ? readJsonc(CONFIG_PATH) : {};
242
+ const models = { ...DEFAULT_CFG.models, ...((cfg.models && typeof cfg.models === "object") ? cfg.models : {}) };
243
+ models[type] = model;
244
+ saveCfg({ models });
245
+ return T("model_switched", { type, model });
246
+ }
247
+ function statusCmd() {
248
+ const c = getCfg();
249
+ const fmt = (t) => c.models[t]?.model || T("model_not_set");
250
+ return T("status", { s: ENABLED ? T("enabled") : T("disabled"), m: fmt("vision"), i: fmt("image"), v: fmt("video"), t: fmt("tts") });
251
+ }
252
+ function listCmd(args, sessionID) {
253
+ const arg = args.join(" ");
254
+ let targetSid = sessionID;
255
+ let rest = arg;
256
+ if (arg === "main" || arg.startsWith("main ")) {
257
+ targetSid = getRootSession(sessionID);
258
+ rest = arg === "main" ? "" : arg.slice(5).trim();
259
+ }
260
+ const tokens = rest.split(/\s+/).filter(Boolean);
261
+ let filter = null;
262
+ let countStr = "1";
263
+ if (tokens[0] && CACHE_TYPES.includes(tokens[0])) {
264
+ filter = tokens[0];
265
+ countStr = tokens[1] || "all";
266
+ }
267
+ else if (tokens[0]) {
268
+ countStr = tokens[0];
269
+ }
270
+ const types = filter ? [filter] : CACHE_TYPES;
271
+ const lines = [];
272
+ for (const type of types) {
273
+ const store = readTypeStore(targetSid, type);
274
+ const entries = Object.values(store.files);
275
+ if (entries.length === 0)
276
+ continue;
277
+ let show = entries;
278
+ if (countStr !== "all") {
279
+ const n = parseInt(countStr, 10);
280
+ if (!isNaN(n) && n > 0)
281
+ show = entries.slice(-n);
282
+ }
283
+ lines.push(` ${type}:`);
284
+ for (const f of show) {
285
+ const src = f.msgId ? `msg_${f.msgId.slice(-8)}` : type === "input" ? "input" : "generated";
286
+ lines.push(` ${f.filename} (${type}:${f.id}) [${src}]`);
287
+ }
288
+ }
289
+ if (lines.length === 0)
290
+ return `${targetSid}: ${T("no_cache")}`;
291
+ return `${targetSid}:\n${lines.join("\n")}`;
292
+ }
293
+ async function handleCommand(raw, context) {
294
+ const tokens = stringArgv(raw);
295
+ const [cmd, ...rest] = tokens;
296
+ if (!cmd)
297
+ return helpText();
298
+ switch (cmd) {
299
+ case "help":
300
+ case "h":
301
+ case "-h":
302
+ case "--help": return helpText();
303
+ case "analyze":
304
+ case "a": return analyzeCmd(rest, context);
305
+ case "imagine":
306
+ case "i": return imagineCmd(rest, context);
307
+ case "video":
308
+ case "v": return videoCmd(rest, context);
309
+ case "tts":
310
+ case "t": return ttsCmd(rest, context);
311
+ case "list":
312
+ case "l": return listCmd(rest, context.sessionID);
313
+ case "providers":
314
+ case "list-provider":
315
+ case "p": return providersCmd();
316
+ case "set":
317
+ case "set-provider":
318
+ case "s": return setCmd(rest);
319
+ case "status": return statusCmd();
320
+ case "enable":
321
+ setEnabled(true);
322
+ return T("enabled");
323
+ case "disable":
324
+ setEnabled(false);
325
+ return T("disabled");
326
+ case "enable-save":
327
+ saveCfg({ enabled: true });
328
+ return T("enabled");
329
+ case "disable-save":
330
+ saveCfg({ enabled: false });
331
+ return T("disabled");
332
+ default:
333
+ return T("unknown_cmd", { cmd: cmd.slice(0, 50) }) + "\n\n" + helpText();
334
+ }
335
+ }
336
+ export const fileToolCli = tool({
337
+ description: DESC.file_tool[LANG],
338
+ args: { command: tool.schema.string().optional().describe(DESC.file_tool_args[LANG]) },
339
+ execute: async ({ command }, context) => {
340
+ return handleCommand(command ?? "help", context);
341
+ },
342
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaoqiong0v0/opencode-file-tool",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
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": [
@@ -28,7 +28,8 @@
28
28
  "@opencode-ai/plugin": "*"
29
29
  },
30
30
  "dependencies": {
31
- "@xiaoqiong0v0/opencode-plugin-logger": "^1.0.2"
31
+ "@xiaoqiong0v0/opencode-plugin-logger": "^1.0.4",
32
+ "string-argv": "^0.3.2"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/node": "^26.1.1",
@@ -1,94 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { DESC, T, LANG } from "../i18n.js";
5
- import { requireModelCfg, getCfg } from "../config.js";
6
- import { findFileInChain, readFileData } from "../cache.js";
7
- const CACHE_TYPES_LIST = ["input", "image", "video", "tts"];
8
- export const analyzeImageTool = tool({
9
- description: DESC.analyze_image[LANG],
10
- args: {
11
- source: tool.schema.enum(["file_path", "base64"]).describe(DESC.analyze_args_source[LANG]),
12
- data: tool.schema.string().describe(DESC.analyze_args_data[LANG]),
13
- prompt: tool.schema.string().optional().describe(DESC.analyze_args_prompt[LANG]),
14
- },
15
- execute: async ({ source, data, prompt }, context) => {
16
- let imageUrl, fileName = "";
17
- if (source === "file_path" && data.startsWith("file_id:")) {
18
- const segs = data.slice(8).split(":");
19
- let type = "input";
20
- let fid;
21
- if (segs.length === 2 && CACHE_TYPES_LIST.includes(segs[0])) {
22
- type = segs[0];
23
- fid = parseInt(segs[1], 10);
24
- }
25
- else {
26
- fid = parseInt(segs[0], 10);
27
- }
28
- const found = findFileInChain(context.sessionID, type, fid);
29
- if (!found) {
30
- context.metadata?.({ title: T("meta_failed") });
31
- return T("file_id_not_found", { id: `${type}:${fid}` });
32
- }
33
- const file = found.file;
34
- if (!file.cached) {
35
- context.metadata?.({ title: T("meta_skip") });
36
- return T("uncached_hint", { id: `${type}:${fid}` });
37
- }
38
- if (!file.mime.startsWith("image/")) {
39
- context.metadata?.({ title: T("meta_skip") });
40
- return T("not_an_image", { name: file.filename, mime: file.mime });
41
- }
42
- fileName = file.filename;
43
- imageUrl = readFileData(context.sessionID, type, fid) || "";
44
- if (!imageUrl) {
45
- context.metadata?.({ title: T("meta_failed") });
46
- return T("file_data_not_found", { id: `${type}:${fid}` });
47
- }
48
- prompt = prompt || T("describe_image", { name: fileName });
49
- }
50
- else if (source === "file_path") {
51
- if (!existsSync(data)) {
52
- const tryPath = join(context.directory, data);
53
- if (existsSync(tryPath))
54
- data = tryPath;
55
- }
56
- if (!existsSync(data)) {
57
- context.metadata?.({ title: T("meta_not_found") });
58
- return T("file_not_found", { path: data });
59
- }
60
- const ext = data.split(".").pop()?.toLowerCase() || "";
61
- const mimeMap = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", bmp: "image/bmp", gif: "image/gif", webp: "image/webp" };
62
- const mime = mimeMap[ext] || "image/png";
63
- fileName = data.split(/[/\\]/).pop() || "";
64
- imageUrl = `data:${mime};base64,${readFileSync(data).toString("base64")}`;
65
- }
66
- else if (source === "base64") {
67
- imageUrl = `data:image/png;base64,${data.replace(/^data:image\/\w+;base64,/, "")}`;
68
- }
69
- else {
70
- return T("unsupported_source", { source });
71
- }
72
- try {
73
- const cfg = requireModelCfg("vision");
74
- const resp = await fetch(`${cfg.baseURL}/chat/completions`, {
75
- method: "POST",
76
- headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json" },
77
- body: JSON.stringify({ model: cfg.modelId, messages: [{ role: "user", content: [{ type: "text", text: prompt || T("vision_prompt_default") }, { type: "image_url", image_url: { url: imageUrl } }] }], max_tokens: getCfg().maxTokens }),
78
- signal: AbortSignal.timeout(getCfg().timeout || 60000),
79
- });
80
- if (!resp.ok)
81
- throw new Error(T("err_api", { status: String(resp.status), msg: (await resp.text().catch(() => "unknown")).slice(0, 200) }));
82
- const data = await resp.json();
83
- const msg = data.choices?.[0]?.message;
84
- const result = msg?.content || msg?.reasoning_content || T("empty_response");
85
- context.metadata?.({ title: `[Vision] ${fileName || T("meta_image")}`, metadata: { sessionID: context.sessionID, messageID: context.messageID } });
86
- return "[Vision] " + result;
87
- }
88
- catch (e) {
89
- const msg = e instanceof Error ? e.message : String(e);
90
- context.metadata?.({ title: T("meta_error") });
91
- return `[Vision Error] ${msg}`;
92
- }
93
- },
94
- });
@@ -1,105 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { existsSync } from "node:fs";
3
- import { DESC, T, LANG } from "../i18n.js";
4
- import { CONFIG_PATH, MODEL_TYPES, CACHE_TYPES, DEFAULT_CFG, getCfg, getProviderNames, readJsonc, ENABLED, setEnabled, saveCfg } from "../config.js";
5
- import { readTypeStore, getRootSession } from "../cache.js";
6
- import { localModelList, listModelsApi, currentModelSummary } from "./file_tool_helpers.js";
7
- export const fileTool = tool({
8
- description: DESC.file_tool[LANG],
9
- args: { command: tool.schema.string().describe(DESC.file_tool_args[LANG]) },
10
- execute: async ({ command }, context) => {
11
- const cmd = command.trim();
12
- if (cmd === "list-provider") {
13
- const providerNames = getProviderNames();
14
- const models = await listModelsApi(providerNames);
15
- const fallbackModels = localModelList();
16
- for (const m of fallbackModels)
17
- if (!models.includes(m))
18
- models.push(m);
19
- const modelLines = models.map(m => " " + m).join("\n");
20
- return T("current_model", { model: currentModelSummary(), list: modelLines });
21
- }
22
- if (cmd.startsWith("set-provider ")) {
23
- const arg = cmd.slice(13).trim();
24
- if (!arg)
25
- return T("specify_model");
26
- let type = "vision";
27
- let model = arg;
28
- const colon = arg.indexOf(":");
29
- if (colon > 0 && MODEL_TYPES.includes(arg.slice(0, colon))) {
30
- type = arg.slice(0, colon);
31
- model = arg.slice(colon + 1).trim();
32
- }
33
- if (!model)
34
- return T("specify_model");
35
- const cfg = existsSync(CONFIG_PATH) ? readJsonc(CONFIG_PATH) : {};
36
- const models = { ...DEFAULT_CFG.models, ...((cfg.models && typeof cfg.models === "object") ? cfg.models : {}) };
37
- models[type] = model;
38
- saveCfg({ models });
39
- return T("model_switched", { type, model });
40
- }
41
- if (cmd === "disable") {
42
- setEnabled(false);
43
- return T("disabled");
44
- }
45
- if (cmd === "enable") {
46
- setEnabled(true);
47
- return T("enabled");
48
- }
49
- if (cmd === "disable-save") {
50
- saveCfg({ enabled: false });
51
- return T("disabled");
52
- }
53
- if (cmd === "enable-save") {
54
- saveCfg({ enabled: true });
55
- return T("enabled");
56
- }
57
- if (cmd === "status") {
58
- const c = getCfg();
59
- const fmt = (t) => c.models[t]?.model || T("model_not_set");
60
- return T("status", { s: ENABLED ? T("enabled") : T("disabled"), m: fmt("vision"), i: fmt("image"), v: fmt("video"), t: fmt("tts") });
61
- }
62
- if (cmd === "list-cache" || cmd.startsWith("list-cache ")) {
63
- const rest = cmd === "list-cache" ? "" : cmd.slice(11).trim();
64
- let targetSid = context.sessionID;
65
- let arg = rest;
66
- if (arg === "main" || arg.startsWith("main ")) {
67
- targetSid = getRootSession(context.sessionID);
68
- arg = arg === "main" ? "" : arg.slice(5).trim();
69
- }
70
- const tokens = arg.split(/\s+/).filter(Boolean);
71
- let filter = null;
72
- let countStr = "1";
73
- if (tokens[0] && CACHE_TYPES.includes(tokens[0])) {
74
- filter = tokens[0];
75
- countStr = tokens[1] || "all";
76
- }
77
- else if (tokens[0]) {
78
- countStr = tokens[0];
79
- }
80
- const types = filter ? [filter] : CACHE_TYPES;
81
- const lines = [];
82
- for (const type of types) {
83
- const store = readTypeStore(targetSid, type);
84
- const entries = Object.values(store.files);
85
- if (entries.length === 0)
86
- continue;
87
- let show = entries;
88
- if (countStr !== "all") {
89
- const n = parseInt(countStr, 10);
90
- if (!isNaN(n) && n > 0)
91
- show = entries.slice(-n);
92
- }
93
- lines.push(` ${type}:`);
94
- for (const f of show) {
95
- const src = f.msgId ? `msg_${f.msgId.slice(-8)}` : type === "input" ? "input" : "generated";
96
- lines.push(` ${f.filename} (${type}:${f.id}) [${src}]`);
97
- }
98
- }
99
- if (lines.length === 0)
100
- return `${targetSid}: ${T("no_cache")}`;
101
- return `${targetSid}:\n${lines.join("\n")}`;
102
- }
103
- return T("unknown_cmd", { cmd });
104
- },
105
- });
@@ -1,107 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { DESC, T, LANG } from "../i18n.js";
3
- import { requireModelCfg } from "../config.js";
4
- import { readTypeStore, registerGeneratedFile, fetchToBuffer, buildGenFilename, extForMime } from "../cache.js";
5
- import { generateVideo } from "../providers.js";
6
- import { genError } from "../utils.js";
7
- const GEN_TIMEOUT = 300000;
8
- export const textToImageTool = tool({
9
- description: DESC.text_to_image[LANG],
10
- args: {
11
- prompt: tool.schema.string().describe(DESC.gen_args_prompt[LANG]),
12
- size: tool.schema.string().optional().describe(DESC.gen_args_size[LANG]),
13
- },
14
- execute: async ({ prompt, size }, context) => {
15
- try {
16
- const cfg = requireModelCfg("image");
17
- const body = { model: cfg.modelId, prompt };
18
- if (size)
19
- body.size = size;
20
- const resp = await fetch(`${cfg.baseURL}/images/generations`, {
21
- method: "POST",
22
- headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json" },
23
- body: JSON.stringify(body),
24
- signal: AbortSignal.timeout(GEN_TIMEOUT),
25
- });
26
- if (!resp.ok)
27
- throw new Error(T("err_api", { status: String(resp.status), msg: (await resp.text().catch(() => "unknown")).slice(0, 200) }));
28
- const data = await resp.json();
29
- const item = data?.data?.[0];
30
- if (!item)
31
- throw new Error(T("gen_no_result"));
32
- let buffer, mime = "image/png";
33
- if (item.b64_json)
34
- buffer = Buffer.from(item.b64_json, "base64");
35
- else if (item.url) {
36
- const d = await fetchToBuffer(item.url);
37
- buffer = d.buffer;
38
- mime = d.mime;
39
- }
40
- else
41
- throw new Error(T("gen_no_result"));
42
- const store = readTypeStore(context.sessionID, "image");
43
- const r = registerGeneratedFile(context.sessionID, "image", buildGenFilename("image", mime, store.nextId), mime, context.messageID, buffer);
44
- context.metadata?.({ title: "[ImageGen]", metadata: { sessionID: context.sessionID, messageID: context.messageID } });
45
- return T("image_generated", { name: `image_${r.id}.${extForMime(mime)}`, id: `image:${r.id}`, path: r.path });
46
- }
47
- catch (e) {
48
- context.metadata?.({ title: T("meta_error") });
49
- return genError(e);
50
- }
51
- },
52
- });
53
- export const textToVideoTool = tool({
54
- description: DESC.text_to_video[LANG],
55
- args: {
56
- prompt: tool.schema.string().describe(DESC.gen_args_prompt[LANG]),
57
- duration: tool.schema.number().optional().describe(DESC.gen_args_duration[LANG]),
58
- },
59
- execute: async ({ prompt, duration }, context) => {
60
- try {
61
- const cfg = requireModelCfg("video");
62
- const url = await generateVideo(cfg, prompt, duration);
63
- const d = await fetchToBuffer(url);
64
- const store = readTypeStore(context.sessionID, "video");
65
- const r = registerGeneratedFile(context.sessionID, "video", buildGenFilename("video", d.mime, store.nextId), d.mime, context.messageID, d.buffer);
66
- context.metadata?.({ title: "[VideoGen]", metadata: { sessionID: context.sessionID, messageID: context.messageID } });
67
- return T("video_generated", { name: `video_${r.id}.${extForMime(d.mime)}`, id: `video:${r.id}`, path: r.path });
68
- }
69
- catch (e) {
70
- context.metadata?.({ title: T("meta_error") });
71
- return genError(e);
72
- }
73
- },
74
- });
75
- export const textToSpeechTool = tool({
76
- description: DESC.text_to_speech[LANG],
77
- args: {
78
- text: tool.schema.string().describe(DESC.gen_args_prompt[LANG]),
79
- voice: tool.schema.string().optional().describe(DESC.gen_args_voice[LANG]),
80
- },
81
- execute: async ({ text, voice }, context) => {
82
- try {
83
- const cfg = requireModelCfg("tts");
84
- const body = { model: cfg.modelId, input: text };
85
- if (voice)
86
- body.voice = voice;
87
- const resp = await fetch(`${cfg.baseURL}/audio/speech`, {
88
- method: "POST",
89
- headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json" },
90
- body: JSON.stringify(body),
91
- signal: AbortSignal.timeout(GEN_TIMEOUT),
92
- });
93
- if (!resp.ok)
94
- throw new Error(T("err_api", { status: String(resp.status), msg: (await resp.text().catch(() => "unknown")).slice(0, 200) }));
95
- const buf = Buffer.from(await resp.arrayBuffer());
96
- const mime = resp.headers.get("content-type")?.split(";")[0] || "audio/mpeg";
97
- const store = readTypeStore(context.sessionID, "tts");
98
- const r = registerGeneratedFile(context.sessionID, "tts", buildGenFilename("tts", mime, store.nextId), mime, context.messageID, buf);
99
- context.metadata?.({ title: "[TTS]", metadata: { sessionID: context.sessionID, messageID: context.messageID } });
100
- return T("tts_generated", { name: `tts_${r.id}.${extForMime(mime)}`, id: `tts:${r.id}`, path: r.path });
101
- }
102
- catch (e) {
103
- context.metadata?.({ title: T("meta_error") });
104
- return genError(e);
105
- }
106
- },
107
- });