@xiaoqiong0v0/opencode-file-tool 1.0.5 → 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/README.md +49 -43
- package/dist/cache.js +222 -0
- package/dist/config.js +168 -0
- package/dist/file-tool.js +32 -356
- package/dist/i18n.js +67 -0
- package/dist/providers.js +61 -0
- package/dist/tools/analyze.js +94 -0
- package/dist/tools/file_tool.js +105 -0
- package/dist/tools/file_tool_helpers.js +59 -0
- package/dist/tools/generate.js +107 -0
- package/dist/utils.js +32 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,43 +1,49 @@
|
|
|
1
|
-
# opencode-file-tool
|
|
2
|
-
|
|
3
|
-
OpenCode 文件缓存与图片分析插件。自动缓存用户粘贴的图片,通过多模态模型分析,绕过主模型不支持视觉的限制。
|
|
4
|
-
|
|
5
|
-
## 安装
|
|
6
|
-
|
|
7
|
-
在 `opencode.json` 的 `plugin` 数组添加包名:
|
|
8
|
-
|
|
9
|
-
```json
|
|
10
|
-
"plugin": ["@xiaoqiong0v0/opencode-file-tool"]
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
重启 OpenCode 后自动安装。首次使用 `file_tool set-provider <模型名>` 配置视觉模型。
|
|
14
|
-
|
|
15
|
-
## 功能
|
|
16
|
-
|
|
17
|
-
- **文件缓存** — 粘贴图片时自动缓存到 `~/.opencode/plugins-cache/`
|
|
18
|
-
- **图片分析** — 通过 `analyze_image file_id:N` 调用视觉模型分析
|
|
19
|
-
- **会话父子链** — 子 agent 会话可通过 parent 链回退读取祖先会话的缓存
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
| `file_tool
|
|
28
|
-
| `file_tool
|
|
29
|
-
| `
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
|
34
|
-
|
|
35
|
-
|
|
|
36
|
-
|
|
37
|
-
##
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
# opencode-file-tool
|
|
2
|
+
|
|
3
|
+
OpenCode 文件缓存与图片分析插件。自动缓存用户粘贴的图片,通过多模态模型分析,绕过主模型不支持视觉的限制。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
在 `opencode.json` 的 `plugin` 数组添加包名:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
"plugin": ["@xiaoqiong0v0/opencode-file-tool"]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
重启 OpenCode 后自动安装。首次使用 `file_tool set-provider <模型名>` 配置视觉模型。
|
|
14
|
+
|
|
15
|
+
## 功能
|
|
16
|
+
|
|
17
|
+
- **文件缓存** — 粘贴图片时自动缓存到 `~/.opencode/plugins-cache/file-tool/`
|
|
18
|
+
- **图片分析** — 通过 `analyze_image file_id:N` 调用视觉模型分析
|
|
19
|
+
- **会话父子链** — 子 agent 会话可通过 parent 链回退读取祖先会话的缓存
|
|
20
|
+
- **开关控制** — 主模型支持视觉时可用 `disable` 关闭缓存,让模型直接读取图片
|
|
21
|
+
- **多语言** — 支持中/英文提示
|
|
22
|
+
|
|
23
|
+
## 工具
|
|
24
|
+
|
|
25
|
+
| 工具 | 说明 |
|
|
26
|
+
|------|------|
|
|
27
|
+
| `file_tool list-provider` | 列出可用模型 |
|
|
28
|
+
| `file_tool set-provider <model>` | 切换视觉模型 |
|
|
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` | 查看缓存开关状态和当前视觉模型 |
|
|
35
|
+
| `analyze_image file_id:N` | 分析指定图片 |
|
|
36
|
+
|
|
37
|
+
## 命令
|
|
38
|
+
|
|
39
|
+
| 命令 | 说明 |
|
|
40
|
+
|------|------|
|
|
41
|
+
| `/file-tool` | 触发 file_tool 工具 |
|
|
42
|
+
|
|
43
|
+
## 配置
|
|
44
|
+
|
|
45
|
+
`~/.config/opencode/file-tool.jsonc` 在首次启动时自动生成,支持 `enabled` 字段控制缓存开关。
|
|
46
|
+
|
|
47
|
+
## GitHub
|
|
48
|
+
|
|
49
|
+
https://github.com/xiaoqiong0v0/opencode-file-tool
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { T } from "./i18n.js";
|
|
5
|
+
import { CACHE_DIR, CACHE_EXT, CACHE_TYPES, log, MAX_CACHE_MSGS, MAX_GENERATED } from "./config.js";
|
|
6
|
+
export const sessionParents = new Map();
|
|
7
|
+
export const knownSessions = new Set();
|
|
8
|
+
export function getRootSession(sid) {
|
|
9
|
+
let current = sid;
|
|
10
|
+
while (sessionParents.has(current))
|
|
11
|
+
current = sessionParents.get(current);
|
|
12
|
+
return current;
|
|
13
|
+
}
|
|
14
|
+
export function findFileInChain(sid, type, fid) {
|
|
15
|
+
const store = readTypeStore(sid, type);
|
|
16
|
+
const file = store.files[fid];
|
|
17
|
+
if (file)
|
|
18
|
+
return { store, type, file };
|
|
19
|
+
const parentSid = sessionParents.get(sid);
|
|
20
|
+
if (parentSid)
|
|
21
|
+
return findFileInChain(parentSid, type, fid);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function sessionDir(sid) { return join(CACHE_DIR, sid); }
|
|
25
|
+
function filesDir(sid) {
|
|
26
|
+
const d = join(sessionDir(sid), "files");
|
|
27
|
+
if (!existsSync(d))
|
|
28
|
+
mkdirSync(d, { recursive: true });
|
|
29
|
+
return d;
|
|
30
|
+
}
|
|
31
|
+
export function readTypeStore(sid, type) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(readFileSync(join(sessionDir(sid), `${type}.json`), "utf-8"));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return { nextId: 1, files: {} };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function writeTypeStore(sid, type, data) {
|
|
40
|
+
const dir = sessionDir(sid);
|
|
41
|
+
if (!existsSync(dir))
|
|
42
|
+
mkdirSync(dir, { recursive: true });
|
|
43
|
+
writeFileSync(join(dir, `${type}.json`), JSON.stringify(data, null, 2));
|
|
44
|
+
}
|
|
45
|
+
export function readMessages(sid) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(readFileSync(join(sessionDir(sid), "messages.json"), "utf-8")) || [];
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function writeMessages(sid, msgs) {
|
|
54
|
+
const dir = sessionDir(sid);
|
|
55
|
+
if (!existsSync(dir))
|
|
56
|
+
mkdirSync(dir, { recursive: true });
|
|
57
|
+
writeFileSync(join(dir, "messages.json"), JSON.stringify(msgs, null, 2));
|
|
58
|
+
}
|
|
59
|
+
function removeRefs(sid, refs) {
|
|
60
|
+
let removed = 0;
|
|
61
|
+
for (const ref of refs) {
|
|
62
|
+
const [typeStr, idStr] = ref.split(":");
|
|
63
|
+
const type = typeStr;
|
|
64
|
+
const fid = parseInt(idStr, 10);
|
|
65
|
+
if (!CACHE_TYPES.includes(type) || isNaN(fid))
|
|
66
|
+
continue;
|
|
67
|
+
const store = readTypeStore(sid, type);
|
|
68
|
+
if (!store.files[fid])
|
|
69
|
+
continue;
|
|
70
|
+
delete store.files[fid];
|
|
71
|
+
writeTypeStore(sid, type, store);
|
|
72
|
+
const path = join(filesDir(sid), typeFilepath(type, store, fid));
|
|
73
|
+
rm(path, { force: true }).catch(() => { });
|
|
74
|
+
removed++;
|
|
75
|
+
}
|
|
76
|
+
return removed;
|
|
77
|
+
}
|
|
78
|
+
function typeFilepath(type, store, fid) {
|
|
79
|
+
const f = store.files[fid];
|
|
80
|
+
return f ? f.filename : `${type}_${fid}.${CACHE_EXT[type]}`;
|
|
81
|
+
}
|
|
82
|
+
export function deleteSession(sid) {
|
|
83
|
+
const dir = join(CACHE_DIR, sid);
|
|
84
|
+
if (existsSync(dir))
|
|
85
|
+
rmSync(dir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
export function removeMsgCache(sid, msgId) {
|
|
88
|
+
const msgs = readMessages(sid);
|
|
89
|
+
const idx = msgs.findIndex(m => m.msgId === msgId);
|
|
90
|
+
if (idx < 0) {
|
|
91
|
+
log.info(`${sid}: msg ${msgId.slice(-8)} not in cache, skip`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const [msg] = msgs.splice(idx, 1);
|
|
95
|
+
removeRefs(sid, msg.refs);
|
|
96
|
+
writeMessages(sid, msgs);
|
|
97
|
+
log.info(`${sid}: removed msg ${msgId.slice(-8)} (${msg.refs.length} files: ${msg.refs.join(", ")})`);
|
|
98
|
+
}
|
|
99
|
+
function evictOldMessages(sid) {
|
|
100
|
+
const msgs = readMessages(sid);
|
|
101
|
+
if (msgs.length <= MAX_CACHE_MSGS)
|
|
102
|
+
return;
|
|
103
|
+
const expired = msgs.splice(0, msgs.length - MAX_CACHE_MSGS);
|
|
104
|
+
for (const msg of expired)
|
|
105
|
+
removeRefs(sid, msg.refs);
|
|
106
|
+
writeMessages(sid, msgs);
|
|
107
|
+
}
|
|
108
|
+
function evictGenerated(sid, type) {
|
|
109
|
+
if (MAX_GENERATED <= 0)
|
|
110
|
+
return;
|
|
111
|
+
const store = readTypeStore(sid, type);
|
|
112
|
+
const ids = Object.keys(store.files).map(Number).sort((a, b) => a - b);
|
|
113
|
+
const overflow = ids.length - MAX_GENERATED;
|
|
114
|
+
if (overflow <= 0)
|
|
115
|
+
return;
|
|
116
|
+
for (const fid of ids.slice(0, overflow)) {
|
|
117
|
+
delete store.files[fid];
|
|
118
|
+
const path = join(filesDir(sid), typeFilepath(type, store, fid));
|
|
119
|
+
rm(path, { force: true }).catch(() => { });
|
|
120
|
+
const msgs = readMessages(sid);
|
|
121
|
+
for (const m of msgs) {
|
|
122
|
+
const i = m.refs.indexOf(`${type}:${fid}`);
|
|
123
|
+
if (i >= 0)
|
|
124
|
+
m.refs.splice(i, 1);
|
|
125
|
+
}
|
|
126
|
+
writeMessages(sid, msgs);
|
|
127
|
+
}
|
|
128
|
+
writeTypeStore(sid, type, store);
|
|
129
|
+
}
|
|
130
|
+
export function registerFile(sid, type, filename, mime, msgId, buffer) {
|
|
131
|
+
const store = readTypeStore(sid, type);
|
|
132
|
+
const fid = store.nextId++;
|
|
133
|
+
store.files[fid] = { id: fid, filename, mime, msgId, cached: true };
|
|
134
|
+
writeTypeStore(sid, type, store);
|
|
135
|
+
const path = join(filesDir(sid), filename);
|
|
136
|
+
writeFileSync(path, buffer);
|
|
137
|
+
const msgs = readMessages(sid);
|
|
138
|
+
const last = msgs[msgs.length - 1];
|
|
139
|
+
const ref = `${type}:${fid}`;
|
|
140
|
+
if (last && last.msgId === msgId) {
|
|
141
|
+
last.refs.push(ref);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
msgs.push({ msgId, refs: [ref] });
|
|
145
|
+
}
|
|
146
|
+
writeMessages(sid, msgs);
|
|
147
|
+
if (type === "input")
|
|
148
|
+
evictOldMessages(sid);
|
|
149
|
+
else
|
|
150
|
+
evictGenerated(sid, type);
|
|
151
|
+
log.info(`${sid}: registered ${type} file ${filename} as ${ref}`);
|
|
152
|
+
return { id: fid, path };
|
|
153
|
+
}
|
|
154
|
+
export function registerInputFile(sid, filename, mime, msgId, dataUrl) {
|
|
155
|
+
const b64 = dataUrl.replace(/^data:\w+\/\w+;base64,/, "");
|
|
156
|
+
return registerFile(sid, "input", filename, mime, msgId, Buffer.from(b64, "base64"));
|
|
157
|
+
}
|
|
158
|
+
export function registerGeneratedFile(sid, type, filename, mime, msgId, buffer) {
|
|
159
|
+
return registerFile(sid, type, filename, mime, msgId, buffer);
|
|
160
|
+
}
|
|
161
|
+
export function readFileData(sid, type, fid) {
|
|
162
|
+
const found = findFileInChain(sid, type, fid);
|
|
163
|
+
if (!found)
|
|
164
|
+
return null;
|
|
165
|
+
try {
|
|
166
|
+
const buf = readFileSync(join(filesDir(sid), typeFilepath(type, found.store, fid)));
|
|
167
|
+
return `data:${found.file.mime || "image/png"};base64,${buf.toString("base64")}`;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export async function fetchToBuffer(url) {
|
|
174
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(60000) });
|
|
175
|
+
if (!resp.ok)
|
|
176
|
+
throw new Error(T("err_api", { status: String(resp.status), msg: (await resp.text().catch(() => "unknown")).slice(0, 200) }));
|
|
177
|
+
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
178
|
+
const mime = resp.headers.get("content-type")?.split(";")[0] || "application/octet-stream";
|
|
179
|
+
return { buffer, mime };
|
|
180
|
+
}
|
|
181
|
+
export function extForMime(mime) {
|
|
182
|
+
const map = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "video/mp4": "mp4", "video/webm": "webm", "audio/mpeg": "mp3", "audio/wav": "wav", "audio/ogg": "ogg" };
|
|
183
|
+
return map[mime] || "bin";
|
|
184
|
+
}
|
|
185
|
+
export function buildGenFilename(prefix, mime, id) {
|
|
186
|
+
return `${prefix}_${id}.${extForMime(mime)}`;
|
|
187
|
+
}
|
|
188
|
+
export function migrateLegacyCache(sid) {
|
|
189
|
+
const oldPath = join(CACHE_DIR, sid, "files.json");
|
|
190
|
+
if (!existsSync(oldPath))
|
|
191
|
+
return;
|
|
192
|
+
try {
|
|
193
|
+
const old = JSON.parse(readFileSync(oldPath, "utf-8"));
|
|
194
|
+
const store = { nextId: old.nextId || 1, files: {} };
|
|
195
|
+
const msgs = [];
|
|
196
|
+
for (const m of old.messages || []) {
|
|
197
|
+
const refs = [];
|
|
198
|
+
for (const fid of m.fileIds || []) {
|
|
199
|
+
const f = old.files[fid];
|
|
200
|
+
if (!f)
|
|
201
|
+
continue;
|
|
202
|
+
const ext = extForMime(f.mime || "image/png");
|
|
203
|
+
const filename = `input_${fid}.${ext}`;
|
|
204
|
+
const src = join(filesDir(sid), fid + ".b64");
|
|
205
|
+
if (existsSync(src)) {
|
|
206
|
+
const b64 = readFileSync(src, "utf-8");
|
|
207
|
+
writeFileSync(join(filesDir(sid), filename), Buffer.from(b64, "base64"));
|
|
208
|
+
rm(src, { force: true }).catch(() => { });
|
|
209
|
+
}
|
|
210
|
+
store.files[fid] = { id: fid, filename, mime: f.mime || "image/png", msgId: f.msgId || m.msgId, cached: f.cached !== false };
|
|
211
|
+
refs.push(`input:${fid}`);
|
|
212
|
+
}
|
|
213
|
+
msgs.push({ msgId: m.msgId, refs });
|
|
214
|
+
}
|
|
215
|
+
writeTypeStore(sid, "input", store);
|
|
216
|
+
writeMessages(sid, msgs);
|
|
217
|
+
rm(oldPath, { force: true }).then(() => log.info(`${sid}: migrated legacy cache`)).catch(() => { });
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
log.error(`${sid}: migrate failed`, e instanceof Error ? e : Error(String(e)));
|
|
221
|
+
}
|
|
222
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import createLogger from "@xiaoqiong0v0/opencode-plugin-logger";
|
|
4
|
+
import { T, setLang } from "./i18n.js";
|
|
5
|
+
export const MODEL_TYPES = ["vision", "image", "video", "tts"];
|
|
6
|
+
export const CACHE_TYPES = ["input", "image", "video", "tts"];
|
|
7
|
+
export const CACHE_EXT = { input: "bin", image: "png", video: "mp4", tts: "mp3" };
|
|
8
|
+
export const log = createLogger("file-tool");
|
|
9
|
+
export const CONFIG_DIR = process.env.HOME || process.env.USERPROFILE || "";
|
|
10
|
+
export const CONFIG_PATH = join(CONFIG_DIR, ".config/opencode/file-tool.jsonc");
|
|
11
|
+
export const OPENCODE_CONFIG = join(CONFIG_DIR, ".config/opencode/opencode.json");
|
|
12
|
+
export const CACHE_DIR = join(CONFIG_DIR, ".opencode/plugins-cache", "file-tool");
|
|
13
|
+
export let _cfg = null;
|
|
14
|
+
export let MAX_CACHE_MSGS = 3;
|
|
15
|
+
export let MAX_GENERATED = 5;
|
|
16
|
+
export let ENABLED = true;
|
|
17
|
+
export function setEnabled(v) { ENABLED = v; }
|
|
18
|
+
export const FILE_TOOL_CFG_SAMPLE = `{
|
|
19
|
+
"models": {
|
|
20
|
+
"vision": "",
|
|
21
|
+
"image": "",
|
|
22
|
+
"video": "",
|
|
23
|
+
"tts": ""
|
|
24
|
+
},
|
|
25
|
+
"maxTokens": 4096,
|
|
26
|
+
"timeout": 60000,
|
|
27
|
+
"maxCacheMessages": 3,
|
|
28
|
+
"maxGenerated": 5,
|
|
29
|
+
"lang": "en",
|
|
30
|
+
"enabled": true
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
export const DEFAULT_CFG = {
|
|
34
|
+
models: { vision: "", image: "", video: "", tts: "" },
|
|
35
|
+
maxTokens: 4096,
|
|
36
|
+
timeout: 60000,
|
|
37
|
+
maxCacheMessages: 3,
|
|
38
|
+
maxGenerated: 5,
|
|
39
|
+
lang: "en",
|
|
40
|
+
enabled: true,
|
|
41
|
+
};
|
|
42
|
+
export function readJsonc(path) {
|
|
43
|
+
const raw = readFileSync(path, "utf-8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
44
|
+
return JSON.parse(raw);
|
|
45
|
+
}
|
|
46
|
+
export function splitModel(model) {
|
|
47
|
+
const idx = model.indexOf("/");
|
|
48
|
+
return idx >= 0 ? { provider: model.slice(0, idx), modelId: model.slice(idx + 1) } : { provider: "", modelId: model };
|
|
49
|
+
}
|
|
50
|
+
export function getProviderCreds(provider) {
|
|
51
|
+
try {
|
|
52
|
+
const oc = JSON.parse(readFileSync(OPENCODE_CONFIG, "utf-8"));
|
|
53
|
+
const prov = oc.provider?.[provider];
|
|
54
|
+
if (prov?.options?.apiKey && prov?.options?.baseURL)
|
|
55
|
+
return { apiKey: prov.options.apiKey, baseURL: prov.options.baseURL };
|
|
56
|
+
}
|
|
57
|
+
catch { }
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
export function getProviderNames() {
|
|
61
|
+
const names = new Set();
|
|
62
|
+
try {
|
|
63
|
+
const oc = JSON.parse(readFileSync(OPENCODE_CONFIG, "utf-8"));
|
|
64
|
+
for (const [pName, pVal] of Object.entries(oc.provider || {})) {
|
|
65
|
+
const p = pVal.options;
|
|
66
|
+
if (p?.apiKey && p?.baseURL)
|
|
67
|
+
names.add(pName);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
return names;
|
|
72
|
+
}
|
|
73
|
+
export function resolveModelRef(ref, fallbackCreds) {
|
|
74
|
+
let model = "";
|
|
75
|
+
let creds = fallbackCreds;
|
|
76
|
+
if (typeof ref === "string" && ref.trim()) {
|
|
77
|
+
model = ref.trim();
|
|
78
|
+
}
|
|
79
|
+
else if (ref && typeof ref === "object") {
|
|
80
|
+
const o = ref;
|
|
81
|
+
model = o.model || "";
|
|
82
|
+
const k = o.apiKey || fallbackCreds?.apiKey;
|
|
83
|
+
const u = o.baseURL || o.apiBaseUrl || fallbackCreds?.baseURL;
|
|
84
|
+
if (k && u)
|
|
85
|
+
creds = { apiKey: k, baseURL: u };
|
|
86
|
+
}
|
|
87
|
+
if (!model)
|
|
88
|
+
return undefined;
|
|
89
|
+
const { provider, modelId } = splitModel(model);
|
|
90
|
+
if (provider && modelId) {
|
|
91
|
+
const pc = getProviderCreds(provider);
|
|
92
|
+
if (pc)
|
|
93
|
+
return { model, provider, modelId, ...pc };
|
|
94
|
+
}
|
|
95
|
+
if (creds?.apiKey && creds?.baseURL && modelId)
|
|
96
|
+
return { model, provider, modelId, ...creds };
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
export function resolveConfig(raw) {
|
|
100
|
+
const models = {};
|
|
101
|
+
const fallbackCreds = raw.apiKey && (raw.apiBaseUrl || raw.baseURL)
|
|
102
|
+
? { apiKey: raw.apiKey, baseURL: raw.apiBaseUrl || raw.baseURL }
|
|
103
|
+
: undefined;
|
|
104
|
+
const rawModels = (raw.models && typeof raw.models === "object") ? raw.models : {};
|
|
105
|
+
for (const type of MODEL_TYPES) {
|
|
106
|
+
const ref = rawModels[type] ?? (type === "vision" ? raw.model : undefined);
|
|
107
|
+
const mc = resolveModelRef(ref, fallbackCreds);
|
|
108
|
+
if (mc)
|
|
109
|
+
models[type] = mc;
|
|
110
|
+
}
|
|
111
|
+
return { models, maxTokens: raw.maxTokens || 4096, timeout: raw.timeout || 60000, maxCacheMessages: 3, maxGenerated: raw.maxGenerated >= 0 ? raw.maxGenerated : 5, lang: raw.lang === "zh" ? "zh" : "en", enabled: raw.enabled !== false };
|
|
112
|
+
}
|
|
113
|
+
export function loadCfg() {
|
|
114
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
115
|
+
try {
|
|
116
|
+
writeFileSync(CONFIG_PATH, FILE_TOOL_CFG_SAMPLE, "utf-8");
|
|
117
|
+
}
|
|
118
|
+
catch { }
|
|
119
|
+
}
|
|
120
|
+
const raw = existsSync(CONFIG_PATH) ? readJsonc(CONFIG_PATH) : {};
|
|
121
|
+
_cfg = resolveConfig(raw);
|
|
122
|
+
MAX_CACHE_MSGS = (raw.maxCacheMessages > 0) ? raw.maxCacheMessages : 3;
|
|
123
|
+
MAX_GENERATED = raw.maxGenerated >= 0 ? raw.maxGenerated : 5;
|
|
124
|
+
ENABLED = raw.enabled !== false;
|
|
125
|
+
setLang(raw.lang || "en");
|
|
126
|
+
return _cfg;
|
|
127
|
+
}
|
|
128
|
+
export function reloadCfg() { loadCfg(); }
|
|
129
|
+
export function getCfg() {
|
|
130
|
+
if (_cfg)
|
|
131
|
+
return _cfg;
|
|
132
|
+
const raw = existsSync(CONFIG_PATH) ? readJsonc(CONFIG_PATH) : {};
|
|
133
|
+
_cfg = resolveConfig(raw);
|
|
134
|
+
return _cfg;
|
|
135
|
+
}
|
|
136
|
+
export function getModelCfg(type) {
|
|
137
|
+
return getCfg().models[type];
|
|
138
|
+
}
|
|
139
|
+
export function requireModelCfg(type) {
|
|
140
|
+
const cfg = getModelCfg(type);
|
|
141
|
+
if (!cfg)
|
|
142
|
+
throw new Error(T("model_not_configured", { type }));
|
|
143
|
+
return cfg;
|
|
144
|
+
}
|
|
145
|
+
export function saveCfg(updates) {
|
|
146
|
+
const existing = existsSync(CONFIG_PATH) ? readJsonc(CONFIG_PATH) : {};
|
|
147
|
+
const merged = {};
|
|
148
|
+
for (const key of Object.keys(DEFAULT_CFG)) {
|
|
149
|
+
if (key in existing)
|
|
150
|
+
merged[key] = existing[key];
|
|
151
|
+
}
|
|
152
|
+
Object.assign(merged, updates);
|
|
153
|
+
for (const key of Object.keys(DEFAULT_CFG)) {
|
|
154
|
+
if (!(key in merged))
|
|
155
|
+
merged[key] = DEFAULT_CFG[key];
|
|
156
|
+
}
|
|
157
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2));
|
|
158
|
+
reloadCfg();
|
|
159
|
+
}
|
|
160
|
+
export function writeCfgField(key, value) {
|
|
161
|
+
saveCfg({ [key]: value });
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
loadCfg();
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
log.error("初始化失败", e instanceof Error ? e : Error(String(e)));
|
|
168
|
+
}
|