@xiaoqiong0v0/opencode-file-tool 1.0.8 → 1.1.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,254 +1,16 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
3
- import createLogger from "@xiaoqiong0v0/opencode-plugin-logger";
4
- import { rm } from "node:fs/promises";
5
- import { join } from "node:path";
6
- const CONFIG_DIR = process.env.HOME || process.env.USERPROFILE || "";
7
- const CONFIG_PATH = join(CONFIG_DIR, ".config/opencode/file-tool.jsonc");
8
- const OPENCODE_CONFIG = join(CONFIG_DIR, ".config/opencode/opencode.json");
9
- const CACHE_DIR = join(CONFIG_DIR, ".opencode/plugins-cache", "file-tool");
10
- const log = createLogger("file-tool");
11
- let _cfg = null;
12
- const FILE_TOOL_CFG_SAMPLE = `{
13
- // 视觉分析模型(provider/modelId),file_tool set-provider 切换
14
- "model": "",
15
- "maxTokens": 4096,
16
- "timeout": 60000,
17
- "maxFileSizeMB": 20,
18
- // 缓存消息数量上限,超过则删除最早的
19
- "maxCacheMessages": 3,
20
- // 工具提示语言:zh=中文, en=English
21
- "lang": "en",
22
- // 是否启用图片缓存
23
- "enabled": true
24
- }
25
- `;
26
- let MAX_CACHE_MSGS = 3;
27
- let LANG = "en";
28
- let ENABLED = true;
29
- const TX = {
30
- file_not_found: { zh: "文件不存在: {path}", en: "File not found: {path}" },
31
- file_id_not_found: { zh: "文件ID不存在: {id}", en: "File ID not found: {id}" },
32
- file_data_not_found: { zh: "文件数据不存在: {id}", en: "File data not found: {id}" },
33
- not_an_image: { zh: "不是图片文件: {name} ({mime})", en: "Not an image: {name} ({mime})" },
34
- unsupported_source: { zh: "不支持的图片来源: {source}", en: "Unsupported source: {source}" },
35
- describe_image: { zh: "请详细描述这张图片({name})的内容", en: "Describe this image ({name})" },
36
- current_model: { zh: "当前模型: {model}\n可用模型:\n{list}", en: "Current model: {model}\nAvailable models:\n{list}" },
37
- model_not_set: { zh: "未设置", en: "not set" },
38
- model_switched: { zh: "视觉模型已切换为: {model}", en: "Vision model set to: {model}" },
39
- specify_model: { zh: "请指定模型名", en: "Specify a model name" },
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" },
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" },
42
- meta_failed: { zh: "分析失败", en: "Failed" },
43
- meta_skip: { zh: "跳过", en: "Skip" },
44
- meta_not_found: { zh: "文件不存在", en: "Not found" },
45
- meta_image: { zh: "图片", en: "Image" },
46
- meta_error: { zh: "分析出错", en: "Error" },
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" },
52
- vision_prompt_default: { zh: "请详细描述这张图片的内容,返回格式: [文件名] 描述", en: "Describe this image in detail, format: [filename] description" },
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" },
54
- err_api: { zh: "API {status}: {msg}", en: "API {status}: {msg}" },
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" },
58
- cmd_desc: { zh: "切换视觉分析模型", en: "Switch vision analysis model" },
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." },
60
- };
61
- const T = (key, params) => {
62
- const entry = TX[key] || { zh: key, en: key };
63
- const t = LANG === "zh" ? entry.zh : entry.en;
64
- if (!params)
65
- return t;
66
- return Object.entries(params).reduce((s, [k, v]) => s.replace(`{${k}}`, v), t);
67
- };
68
- function loadCfg() {
69
- if (!existsSync(CONFIG_PATH)) {
70
- try {
71
- writeFileSync(CONFIG_PATH, FILE_TOOL_CFG_SAMPLE, "utf-8");
72
- }
73
- catch { }
74
- }
75
- const raw = existsSync(CONFIG_PATH) ? readJsonc(CONFIG_PATH) : {};
76
- _cfg = resolveConfig(raw);
77
- MAX_CACHE_MSGS = (raw.maxCacheMessages > 0) ? raw.maxCacheMessages : 3;
78
- LANG = (raw.lang === "zh" ? "zh" : "en");
79
- ENABLED = raw.enabled !== false;
80
- return _cfg;
81
- }
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
- }
1
+ import { T } from "./i18n.js";
2
+ import { log, ENABLED, loadCfg } from "./config.js";
3
+ 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";
89
7
  try {
90
8
  loadCfg();
91
9
  }
92
10
  catch (e) {
93
11
  log.error("初始化失败", e instanceof Error ? e : Error(String(e)));
94
12
  }
95
- const DESC = {
96
- analyze_image: {
97
- zh: "用多模态模型分析图片。先调 file_tool list-cache 拿到文件ID,再用 file_id:N 分析。",
98
- en: "Analyze images with multimodal model. Call file_tool list-cache first to get file IDs, then use file_id:N.",
99
- },
100
- file_tool: {
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.",
103
- },
104
- file_tool_args: {
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",
107
- },
108
- analyze_args_source: { zh: "file_path=file_id:N", en: "file_path=file_id:N" },
109
- analyze_args_data: { zh: "file_id:N 或 base64", en: "file_id:N or base64" },
110
- analyze_args_prompt: { zh: "分析提示", en: "prompt" },
111
- };
112
- function getCfg() {
113
- if (_cfg)
114
- return _cfg;
115
- const raw = existsSync(CONFIG_PATH) ? readJsonc(CONFIG_PATH) : {};
116
- _cfg = resolveConfig(raw);
117
- return _cfg;
118
- }
119
- function resolveConfig(fileConfig) {
120
- const model = fileConfig.model;
121
- if (!model)
122
- throw new Error(T("config_error"));
123
- if (fileConfig.apiKey && fileConfig.apiBaseUrl) {
124
- const mId = model.includes("/") ? model.split("/").pop() : model;
125
- return { model, apiKey: fileConfig.apiKey, baseURL: fileConfig.apiBaseUrl, modelId: mId, maxTokens: fileConfig.maxTokens || 4096, timeout: fileConfig.timeout || 60000 };
126
- }
127
- if (model.includes("/")) {
128
- const [provider, modelId] = model.split("/");
129
- try {
130
- const raw = readFileSync(OPENCODE_CONFIG, "utf-8");
131
- const oc = JSON.parse(raw);
132
- const prov = oc.provider?.[provider];
133
- if (prov?.options?.apiKey && prov?.options?.baseURL)
134
- return { model, apiKey: prov.options.apiKey, baseURL: prov.options.baseURL, modelId, maxTokens: fileConfig.maxTokens || 4096, timeout: fileConfig.timeout || 60000 };
135
- }
136
- catch { }
137
- }
138
- throw new Error(T("err_resolve_config", { model }));
139
- }
140
- function readJsonc(path) {
141
- const raw = readFileSync(path, "utf-8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
142
- return JSON.parse(raw);
143
- }
144
- async function callVisionApi(imageUrl, prompt) {
145
- const cfg = getCfg();
146
- const resp = await fetch(`${cfg.baseURL}/chat/completions`, {
147
- method: "POST",
148
- headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json" },
149
- 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: cfg.maxTokens }),
150
- signal: AbortSignal.timeout(cfg.timeout || 60000),
151
- });
152
- if (!resp.ok)
153
- throw new Error(T("err_api", { status: String(resp.status), msg: (await resp.text().catch(() => "unknown")).slice(0, 200) }));
154
- const data = await resp.json();
155
- const msg = data.choices?.[0]?.message;
156
- return msg?.content || msg?.reasoning_content || T("empty_response");
157
- }
158
- const sessionParents = new Map();
159
- const knownSessions = new Set();
160
- function getRootSession(sid) {
161
- let current = sid;
162
- while (sessionParents.has(current))
163
- current = sessionParents.get(current);
164
- return current;
165
- }
166
- function findFileInChain(sid, fid) {
167
- const store = readSession(sid);
168
- const file = store.files[fid];
169
- if (file)
170
- return { store, file };
171
- const parentSid = sessionParents.get(sid);
172
- if (parentSid)
173
- return findFileInChain(parentSid, fid);
174
- return null;
175
- }
176
- function sessionDir(sid) { return join(CACHE_DIR, sid); }
177
- function filesDir(sid) {
178
- const d = join(sessionDir(sid), "files");
179
- if (!existsSync(d))
180
- mkdirSync(d, { recursive: true });
181
- return d;
182
- }
183
- function readSession(sid) {
184
- try {
185
- return JSON.parse(readFileSync(join(sessionDir(sid), "files.json"), "utf-8"));
186
- }
187
- catch {
188
- return { nextId: 1, files: {}, messages: [] };
189
- }
190
- }
191
- function writeSession(sid, data) {
192
- const dir = sessionDir(sid);
193
- if (!existsSync(dir))
194
- mkdirSync(dir, { recursive: true });
195
- const msgs = data.messages || [];
196
- if (msgs.length > MAX_CACHE_MSGS) {
197
- const expired = msgs.splice(0, msgs.length - MAX_CACHE_MSGS);
198
- for (const msg of expired) {
199
- for (const fid of (msg.fileIds || [])) {
200
- delete data.files[fid];
201
- const path = join(dir, "files", fid + ".b64");
202
- rm(path, { force: true }).then(() => {
203
- log.info(`${sid}: Deleted file ${path}`);
204
- }).catch((err) => {
205
- log.error(`${sid}: Failed to delete file ${path}`, err);
206
- });
207
- }
208
- }
209
- }
210
- writeFileSync(join(dir, "files.json"), JSON.stringify(data, null, 2));
211
- }
212
- function writeFileData(sid, fid, url) {
213
- const b64 = url.replace(/^data:\w+\/\w+;base64,/, "");
214
- writeFileSync(join(filesDir(sid), fid + ".b64"), b64, "utf-8");
215
- }
216
- function readFileData(sid, fid) {
217
- try {
218
- const b64 = readFileSync(join(filesDir(sid), fid + ".b64"), "utf-8");
219
- const meta = readSession(sid).files[fid];
220
- return `data:${meta?.mime || "image/png"};base64,${b64}`;
221
- }
222
- catch {
223
- const parentSid = sessionParents.get(sid);
224
- if (parentSid)
225
- return readFileData(parentSid, fid);
226
- return null;
227
- }
228
- }
229
- function deleteSession(sid) {
230
- const dir = sessionDir(sid);
231
- if (existsSync(dir))
232
- rmSync(dir, { recursive: true, force: true });
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
- }
250
- // V1 export:工具 + 事件
251
- export const FileTool = async () => {
13
+ export const fileToolPlugin = async () => {
252
14
  log.loaded();
253
15
  return {
254
16
  config: async (config) => {
@@ -263,6 +25,7 @@ export const FileTool = async () => {
263
25
  knownSessions.add(sid);
264
26
  if (props?.parentID)
265
27
  sessionParents.set(sid, props.parentID);
28
+ migrateLegacyCache(sid);
266
29
  }
267
30
  if (event.type === "session.updated" && sid) {
268
31
  if (!knownSessions.has(sid))
@@ -282,25 +45,15 @@ export const FileTool = async () => {
282
45
  if (part?.type === "file" && (part?.mime || "").startsWith("image/")) {
283
46
  const fn = (part.filename || part.name || "");
284
47
  if (fn && sid) {
285
- const data = readSession(sid);
286
- const fid = data.nextId++;
287
48
  const msgId = (part.messageID || "");
288
- data.files[fid] = { id: fid, filename: fn, mime: part.mime, msgId, cached: ENABLED };
289
49
  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)`);
295
- const msgs = data.messages;
296
- const last = msgs[msgs.length - 1];
297
- if (last && last.msgId === msgId) {
298
- last.fileIds.push(fid);
50
+ const ext = extForMime(part.mime || "image/png");
51
+ const r = registerInputFile(sid, `input_${Date.now() % 100000}.${ext}`, part.mime || "image/png", msgId, (part.url || ""));
52
+ log.info(`${sid}: cached ${fn} as input:${r.id}`);
299
53
  }
300
54
  else {
301
- msgs.push({ msgId, fileIds: [fid] });
55
+ log.info(`${sid}: skip cache ${fn} (disabled)`);
302
56
  }
303
- writeSession(sid, data);
304
57
  }
305
58
  }
306
59
  }
@@ -318,160 +71,12 @@ export const FileTool = async () => {
318
71
  }
319
72
  },
320
73
  tool: {
321
- analyze_image: tool({
322
- description: DESC.analyze_image[LANG],
323
- args: {
324
- source: tool.schema.enum(["file_path", "base64"]).describe(DESC.analyze_args_source[LANG]),
325
- data: tool.schema.string().describe(DESC.analyze_args_data[LANG]),
326
- prompt: tool.schema.string().optional().describe(DESC.analyze_args_prompt[LANG]),
327
- },
328
- execute: async ({ source, data, prompt }, context) => {
329
- let imageUrl, fileName = "";
330
- if (source === "file_path" && data.startsWith("file_id:")) {
331
- const fid = parseInt(data.slice(8), 10);
332
- const found = findFileInChain(context.sessionID, fid);
333
- if (!found) {
334
- context.metadata?.({ title: T("meta_failed") });
335
- return T("file_id_not_found", { id: String(fid) });
336
- }
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
- }
342
- if (!file.mime.startsWith("image/")) {
343
- context.metadata?.({ title: T("meta_skip") });
344
- return T("not_an_image", { name: file.filename, mime: file.mime });
345
- }
346
- fileName = file.filename;
347
- imageUrl = readFileData(context.sessionID, fid) || "";
348
- if (!imageUrl) {
349
- context.metadata?.({ title: T("meta_failed") });
350
- return T("file_data_not_found", { id: String(fid) });
351
- }
352
- prompt = prompt || T("describe_image", { name: fileName });
353
- }
354
- else if (source === "file_path") {
355
- if (!existsSync(data)) {
356
- const tryPath = join(context.directory, data);
357
- if (existsSync(tryPath))
358
- data = tryPath;
359
- }
360
- if (!existsSync(data)) {
361
- context.metadata?.({ title: T("meta_not_found") });
362
- return T("file_not_found", { path: data });
363
- }
364
- const ext = data.split(".").pop()?.toLowerCase() || "";
365
- const mimeMap = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", bmp: "image/bmp", gif: "image/gif", webp: "image/webp" };
366
- const mime = mimeMap[ext] || "image/png";
367
- fileName = data.split(/[/\\]/).pop() || "";
368
- imageUrl = `data:${mime};base64,${readFileSync(data).toString("base64")}`;
369
- }
370
- else if (source === "base64") {
371
- imageUrl = `data:image/png;base64,${data.replace(/^data:image\/\w+;base64,/, "")}`;
372
- }
373
- else {
374
- return T("unsupported_source", { source });
375
- }
376
- try {
377
- const result = await callVisionApi(imageUrl, prompt || "");
378
- context.metadata?.({ title: `[Vision] ${fileName || T("meta_image")}`, metadata: { sessionID: context.sessionID, messageID: context.messageID } });
379
- return "[Vision] " + result;
380
- }
381
- catch (e) {
382
- const msg = e instanceof Error ? e.message : String(e);
383
- context.metadata?.({ title: T("meta_error") });
384
- return `[Vision Error] ${msg}`;
385
- }
386
- },
387
- }),
388
- file_tool: tool({
389
- description: DESC.file_tool[LANG],
390
- args: { command: tool.schema.string().describe(DESC.file_tool_args[LANG]) },
391
- execute: async ({ command }, context) => {
392
- const cmd = command.trim();
393
- if (cmd === "list-provider") {
394
- const cfg = existsSync(CONFIG_PATH) ? readJsonc(CONFIG_PATH) : {};
395
- const models = [];
396
- const oc = JSON.parse(readFileSync(OPENCODE_CONFIG, "utf-8"));
397
- for (const [pName, pVal] of Object.entries(oc.provider || {}))
398
- for (const mId of Object.keys(pVal.models || {}))
399
- models.push(`${pName}/${mId}`);
400
- return T("current_model", {
401
- model: cfg.model || T("model_not_set"),
402
- list: models.map(m => " " + m).join("\n"),
403
- });
404
- }
405
- if (cmd.startsWith("set-provider ")) {
406
- const model = cmd.slice(13).trim();
407
- if (!model)
408
- return T("specify_model");
409
- const cfg = existsSync(CONFIG_PATH) ? readJsonc(CONFIG_PATH) : {};
410
- cfg.model = model;
411
- delete cfg.apiKey;
412
- delete cfg.apiBaseUrl;
413
- writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
414
- reloadCfg();
415
- return T("model_switched", { model });
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
- }
437
- if (cmd === "list-cache" || cmd.startsWith("list-cache ")) {
438
- const arg = cmd === "list-cache" ? "1" : cmd.slice(11).trim();
439
- let targetSid = context.sessionID;
440
- let limit = arg;
441
- if (arg === "main") {
442
- targetSid = getRootSession(context.sessionID);
443
- limit = "1";
444
- }
445
- if (arg.startsWith("main ")) {
446
- targetSid = getRootSession(context.sessionID);
447
- limit = arg.slice(5).trim();
448
- }
449
- const data = readSession(targetSid);
450
- const msgs = data.messages || [];
451
- if (msgs.length === 0)
452
- return `${targetSid}: ${T("no_cache")}`;
453
- let count = msgs.length;
454
- if (limit !== "all") {
455
- const n = parseInt(limit, 10);
456
- if (!isNaN(n) && n > 0)
457
- count = Math.min(n, count);
458
- }
459
- const show = msgs.slice(-count);
460
- let out = `${targetSid}:\n`;
461
- for (const msg of show) {
462
- out += ` msg_${msg.msgId.slice(-8)}:\n`;
463
- for (const fid of msg.fileIds) {
464
- const f = data.files[fid];
465
- if (f)
466
- out += ` ${f.filename}: ${f.id}${f.cached ? "" : ` (${T("uncached")})`}\n`;
467
- }
468
- }
469
- return out.trim();
470
- }
471
- return T("unknown_cmd", { cmd });
472
- },
473
- }),
74
+ analyze_image: analyzeImageTool,
75
+ text_to_image: textToImageTool,
76
+ text_to_video: textToVideoTool,
77
+ text_to_speech: textToSpeechTool,
78
+ file_tool: fileTool,
474
79
  },
475
80
  };
476
81
  };
477
- export { FileTool as default };
82
+ export default fileToolPlugin;
package/dist/i18n.js ADDED
@@ -0,0 +1,67 @@
1
+ export let LANG = "en";
2
+ export function setLang(lang) { LANG = lang === "zh" ? "zh" : "en"; }
3
+ export const TX = {
4
+ file_not_found: { zh: "文件不存在: {path}", en: "File not found: {path}" },
5
+ file_id_not_found: { zh: "文件ID不存在: {id}", en: "File ID not found: {id}" },
6
+ file_data_not_found: { zh: "文件数据不存在: {id}", en: "File data not found: {id}" },
7
+ not_an_image: { zh: "不是图片文件: {name} ({mime})", en: "Not an image: {name} ({mime})" },
8
+ unsupported_source: { zh: "不支持的图片来源: {source}", en: "Unsupported source: {source}" },
9
+ describe_image: { zh: "请详细描述这张图片({name})的内容", en: "Describe this image ({name})" },
10
+ current_model: { zh: "当前模型:\n{model}\n可用模型:\n{list}", en: "Current models:\n{model}\nAvailable models:\n{list}" },
11
+ model_not_set: { zh: "未设置", en: "not set" },
12
+ model_switched: { zh: "{type} 模型已切换为: {model}", en: "{type} model set to: {model}" },
13
+ model_types: { zh: "vision: 图像分析, image: 文生图, video: 文生视频, tts: 文生语音", en: "vision: analyze image, image: text-to-image, video: text-to-video, tts: text-to-speech" },
14
+ model_usage: { zh: "用法: set-provider <模型名>(vision,默认)或 set-provider <类型>:<模型名>,如 set-provider image:xxx/yyy", en: "Usage: set-provider <model> (vision, default) or set-provider <type>:<model>, e.g. set-provider image:xxx/yyy" },
15
+ specify_model: { zh: "请指定模型名", en: "Specify a model name" },
16
+ model_not_configured: { zh: "{type} 模型未配置,请在 file-tool.jsonc 的 models 中设置(可用 set-provider 切换)", en: "{type} model not configured, set it in models of file-tool.jsonc (use set-provider)" },
17
+ image_generated: { zh: "图片已生成并缓存: {name} (file_id:{id})\n路径: {path}\n用 analyze_image file_id:{id} 查看", en: "Image generated & cached: {name} (file_id:{id})\nPath: {path}\nuse analyze_image file_id:{id}" },
18
+ video_generated: { zh: "视频已生成并缓存: {name} (file_id:{id})\n路径: {path}", en: "Video generated & cached: {name} (file_id:{id})\nPath: {path}" },
19
+ video_pending: { zh: "视频生成中(任务 {id}),已提交,稍后可查", en: "Video generating (task {id}), submitted, check later" },
20
+ video_failed: { zh: "视频生成失败: {msg}", en: "Video generation failed: {msg}" },
21
+ tts_generated: { zh: "语音已生成并缓存: {name} (file_id:{id})\n路径: {path}", en: "Audio generated & cached: {name} (file_id:{id})\nPath: {path}" },
22
+ gen_failed: { zh: "生成失败: {msg}", en: "Generation failed: {msg}" },
23
+ gen_no_result: { zh: "接口未返回生成结果", en: "No generation result from API" },
24
+ gen_download_failed: { zh: "下载生成结果失败: {url}", en: "Failed to download result: {url}" },
25
+ unknown_cmd: { zh: "未知命令: {cmd}\n可用: list-provider, set-provider [<类型>:]<模型名>, list-cache [类型] [数量], enable, disable, enable-save, disable-save, status", en: "Unknown command: {cmd}\nAvailable: list-provider, set-provider [<type>:]<model>, list-cache [type] [count], enable, disable, enable-save, disable-save, status" },
26
+ config_error: { zh: "请在 file-tool.jsonc 中配置 models.{type}(provider/modelId)或 apiKey+apiBaseUrl+model", en: "Set models.{type} (provider/modelId) or apiKey+apiBaseUrl+model in file-tool.jsonc" },
27
+ meta_failed: { zh: "分析失败", en: "Failed" },
28
+ meta_skip: { zh: "跳过", en: "Skip" },
29
+ meta_not_found: { zh: "文件不存在", en: "Not found" },
30
+ meta_image: { zh: "图片", en: "Image" },
31
+ meta_error: { zh: "分析出错", en: "Error" },
32
+ no_cache: { zh: "[] (无缓存)", en: "[] (no cache)" },
33
+ enabled: { zh: "已启用", en: "Enabled" },
34
+ disabled: { zh: "已禁用", en: "Disabled" },
35
+ status: { zh: "图片缓存: {s}\n视觉模型: {m}\n文生图: {i}\n文生视频: {v}\n文生语音: {t}", en: "Image cache: {s}\nVision: {m}\nImage gen: {i}\nVideo gen: {v}\nTTS: {t}" },
36
+ status_cmd: { zh: "查看缓存开关状态", en: "Show cache status" },
37
+ vision_prompt_default: { zh: "请详细描述这张图片的内容,返回格式: [文件名] 描述", en: "Describe this image in detail, format: [filename] description" },
38
+ err_resolve_config: { zh: "无法解析模型配置: {model}。请在 file-tool.jsonc 中配置 models.{type} (provider/modelId) 或 apiKey+apiBaseUrl+model", en: "Cannot resolve model config: {model}. Set models.{type} (provider/modelId) or apiKey+apiBaseUrl+model in file-tool.jsonc" },
39
+ err_api: { zh: "API {status}: {msg}", en: "API {status}: {msg}" },
40
+ empty_response: { zh: "(空)", en: "(empty)" },
41
+ uncached: { zh: "未缓存", en: "uncached" },
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-provider` 列出模型(优先 API 查询),`set-provider [类型:]模型名` 切换模型(类型: vision/image/video/tts),`list-cache [类型] [数量]` 查看缓存,`enable/disable` 临时开关,`enable-save/disable-save` 持久化开关,`status` 查看状态。", en: "Call file_tool tool directly. `list-provider` list models (API-first), `set-provider [type:]model` switch (type: vision/image/video/tts), `list-cache [type] [count]` view cache, `enable/disable` temp toggle, `enable-save/disable-save` persist toggle, `status` show state." },
45
+ };
46
+ export const DESC = {
47
+ analyze_image: { zh: "用多模态模型分析图片。先调 file_tool list-cache 拿到文件ID,再用 file_id:类型:id 分析。", en: "Analyze images with multimodal model. Call file_tool list-cache first to get file IDs, then use file_id:type:id." },
48
+ 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." },
49
+ 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." },
50
+ text_to_speech: { zh: "文生语音:将文本转为语音(TTS),结果缓存并返回 file_id:类型:id。", en: "Text-to-speech: convert text to audio, cached and returned as file_id:type:id." },
51
+ file_tool: { zh: "文件缓存管理。当你在上下文中看到 [Image N] 或收到 Cannot read 图片错误时,立即调 list-cache 获取文件ID,再用 analyze_image file_id:类型:id 分析。主模型能直接读取图片时建议用 `disable` 关闭缓存。", 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:type:id. If the main model can read images directly, use `disable` to turn off caching." },
52
+ file_tool_args: { zh: "list-cache [类型] [数量](类型: input/image/video/tts,如 list-cache image 3),list-provider, set-provider [<类型>:]<模型名>, enable/disable(临时), enable-save/disable-save(持久化), status — main 前缀查主会话(list-cache main [类型] [数量])", en: "list-cache [type] [count] (type: input/image/video/tts, e.g. list-cache image 3), list-provider, set-provider [<type>:]<model>, enable/disable (temp), enable-save/disable-save (persist), status — main prefix for root session (list-cache main [type] [count])" },
53
+ 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)" },
54
+ analyze_args_data: { zh: "file_id:类型:id 或 base64", en: "file_id:type:id or base64" },
55
+ analyze_args_prompt: { zh: "分析提示", en: "prompt" },
56
+ gen_args_prompt: { zh: "生成提示词", en: "generation prompt" },
57
+ gen_args_size: { zh: "图片尺寸(如 1024x1024)", en: "image size (e.g. 1024x1024)" },
58
+ gen_args_duration: { zh: "视频时长(秒)", en: "video duration (seconds)" },
59
+ gen_args_voice: { zh: "音色(如 alloy)", en: "voice (e.g. alloy)" },
60
+ };
61
+ export function T(key, params) {
62
+ const entry = TX[key] || { zh: key, en: key };
63
+ const t = LANG === "zh" ? entry.zh : entry.en;
64
+ if (!params)
65
+ return t;
66
+ return Object.entries(params).reduce((s, [k, v]) => s.replace(`{${k}}`, v), t);
67
+ }
@@ -0,0 +1,61 @@
1
+ import { T } from "./i18n.js";
2
+ import { isAgnesProvider, postJson, getJson } from "./utils.js";
3
+ // 视频生成 - agnes 私有接口
4
+ export async function generateVideoAgnes(cfg, prompt, duration) {
5
+ const dur = duration || 5;
6
+ const fps = 24;
7
+ const frames = dur * fps + 1;
8
+ const data = await postJson(`${cfg.baseURL}/videos`, cfg.apiKey, {
9
+ model: cfg.modelId, prompt, num_frames: Math.min(frames, 441), frame_rate: fps,
10
+ });
11
+ const taskId = data?.id || data?.task_id || data?.video_id;
12
+ if (!taskId)
13
+ throw new Error(T("gen_no_result"));
14
+ const deadline = Date.now() + 600000;
15
+ while (Date.now() < deadline) {
16
+ await new Promise(r => setTimeout(r, 10000));
17
+ const pd = await getJson(`https://apihub.agnes-ai.com/agnesapi?video_id=${taskId}&model_name=${cfg.modelId}`, cfg.apiKey);
18
+ const st = pd?.status || pd?.state || "";
19
+ if (st === "completed") {
20
+ const url = pd?.metadata?.url || pd?.url || "";
21
+ if (url)
22
+ return url;
23
+ }
24
+ else if (st === "failed" || st === "error") {
25
+ throw new Error(T("video_failed", { msg: pd?.error || st }));
26
+ }
27
+ }
28
+ throw new Error(T("video_failed", { msg: "timeout" }));
29
+ }
30
+ // 视频生成 - OpenAI 标准接口
31
+ export async function generateVideoOpenAI(cfg, prompt, duration) {
32
+ const body = { model: cfg.modelId, prompt };
33
+ if (duration)
34
+ body.duration = duration;
35
+ const data = await postJson(`${cfg.baseURL}/videos/generations`, cfg.apiKey, body);
36
+ const taskId = data?.id || data?.task_id || data?.data?.[0]?.id;
37
+ if (!taskId)
38
+ throw new Error(T("gen_no_result"));
39
+ const deadline = Date.now() + 600000;
40
+ while (Date.now() < deadline) {
41
+ await new Promise(r => setTimeout(r, 10000));
42
+ const pd = await getJson(`${cfg.baseURL}/videos/generations/${taskId}`, cfg.apiKey);
43
+ const st = pd?.status || pd?.state || "";
44
+ if (st === "completed" || st === "succeeded") {
45
+ const out = pd?.output?.[0] || pd?.output;
46
+ const url = typeof out === "string" ? out : out?.url || "";
47
+ if (url)
48
+ return url;
49
+ }
50
+ else if (st === "failed" || st === "error") {
51
+ throw new Error(T("video_failed", { msg: pd?.error || st }));
52
+ }
53
+ }
54
+ throw new Error(T("video_failed", { msg: "timeout" }));
55
+ }
56
+ // 视频生成 - 路由:按域名选择适配器
57
+ export async function generateVideo(cfg, prompt, duration) {
58
+ if (isAgnesProvider(cfg.baseURL))
59
+ return generateVideoAgnes(cfg, prompt, duration);
60
+ return generateVideoOpenAI(cfg, prompt, duration);
61
+ }