@yswgaicx/yswg-img-cli 0.1.4 → 0.1.6
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 +50 -3
- package/package.json +1 -1
- package/src/api.js +4 -0
- package/src/cli.js +54 -10
- package/src/config.js +3 -2
- package/src/model-cache.js +162 -0
package/README.md
CHANGED
|
@@ -43,6 +43,18 @@ Use these rules when an AI agent calls the CLI:
|
|
|
43
43
|
yswg-img generate --prompt "一只可爱的白色兔子,粉色背景" --json
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
- Read the local model cache without calling the backend:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
yswg-img models --json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- If model validation fails after backend permissions change, refresh the cache:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
yswg-img models refresh --json
|
|
56
|
+
```
|
|
57
|
+
|
|
46
58
|
- For up to 4 parallel images in one backend request, call:
|
|
47
59
|
|
|
48
60
|
```bash
|
|
@@ -61,6 +73,11 @@ yswg-img tasks recover --task-id <task-id-from-json> --out outputs/recovered --j
|
|
|
61
73
|
- For generated images, keep only local file paths and short summaries in agent
|
|
62
74
|
context. Do not paste image bytes, base64, `data:image/...`, or full tool
|
|
63
75
|
output into the conversation.
|
|
76
|
+
- To fetch an Amazon product gallery by ASIN, call:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
yswg-img amazon asin B0TEST1234 --max 7 --json
|
|
80
|
+
```
|
|
64
81
|
|
|
65
82
|
## Generate
|
|
66
83
|
|
|
@@ -72,6 +89,7 @@ Supported Monkey Genius options mirrored by the CLI:
|
|
|
72
89
|
- Reference images: `--ref ./a.png,https://example.com/b.jpg`
|
|
73
90
|
- Night/off-peak queue submission: `--night`
|
|
74
91
|
- AI tool/template payloads: `--template-code`, `--template-vars-json`
|
|
92
|
+
- Amazon ASIN product gallery lookup: `amazon asin <asin> --max 7`
|
|
75
93
|
- Generation history lookup and timeout recovery: `tasks search`, `tasks recover`
|
|
76
94
|
- History tabs/search/delete/night-cancel: `tasks search --tab ...`, `tasks delete`, `tasks cancel-night`
|
|
77
95
|
|
|
@@ -187,20 +205,48 @@ yswg-img tasks delete --id <record-id> --json
|
|
|
187
205
|
yswg-img tasks cancel-night --id <record-id> --json
|
|
188
206
|
```
|
|
189
207
|
|
|
208
|
+
Fetch Amazon product title and main image gallery by ASIN:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
yswg-img amazon asin B0TEST1234 --max 7 --json
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The command calls `/prod-api/web/amazon/asin/{asin}` and returns the backend
|
|
215
|
+
`AmazonProductVo` shape: `asin`, `title`, and `images`.
|
|
216
|
+
|
|
190
217
|
## Configuration
|
|
191
218
|
|
|
192
|
-
|
|
219
|
+
Runtime flags override environment variables, which override the saved config.
|
|
220
|
+
The app ID is the exception: it is fixed to the `npm` app and cannot be
|
|
221
|
+
overridden.
|
|
193
222
|
|
|
194
223
|
- `YSWG_TOKEN`
|
|
195
224
|
- `YSWG_REFRESH_TOKEN`
|
|
196
|
-
- `YSWG_APP_ID`
|
|
197
225
|
- `YSWG_BASE_URL`
|
|
198
226
|
- `YSWG_WS_PATH`
|
|
199
227
|
|
|
200
|
-
Default app
|
|
228
|
+
Default app name: `npm`.
|
|
229
|
+
Default app ID: `2066371323654864898`.
|
|
230
|
+
The app ID is fixed. `--app-id` and `YSWG_APP_ID` are intentionally rejected.
|
|
201
231
|
Default ratio: `1:1`.
|
|
202
232
|
Default generation model: `gemini-3.1-flash-image-preview` (`groupId=6`, 三代).
|
|
203
233
|
|
|
234
|
+
## Model Cache
|
|
235
|
+
|
|
236
|
+
`yswg-img models --json` reads local metadata from
|
|
237
|
+
`~/.yswg-img-cli/model-cache.json`. If that file does not exist, the CLI falls
|
|
238
|
+
back to built-in `npm` app defaults. This keeps generation validation fast and
|
|
239
|
+
stable for agents.
|
|
240
|
+
|
|
241
|
+
Run this only when backend model permissions or model metadata change:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
yswg-img models refresh --json
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
If generation reports a model/group validation mismatch, refresh the cache
|
|
248
|
+
before submitting the same task again.
|
|
249
|
+
|
|
204
250
|
## Code Structure
|
|
205
251
|
|
|
206
252
|
- `bin/yswg-img.js`: executable entrypoint only.
|
|
@@ -208,5 +254,6 @@ Default generation model: `gemini-3.1-flash-image-preview` (`groupId=6`, 三代)
|
|
|
208
254
|
- `src/generate.js`: generation payloads, validation, history recovery, WebSocket wait, uploads, downloads.
|
|
209
255
|
- `src/api.js`: YSWG HTTP API client.
|
|
210
256
|
- `src/config.js`: config loading, env/flag precedence, email normalization.
|
|
257
|
+
- `src/model-cache.js`: built-in model metadata plus local cache refresh/read helpers.
|
|
211
258
|
- `src/args.js`: minimal CLI argument parsing.
|
|
212
259
|
- `src/image-compress.js`: frontend-compatible reference image compression.
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -65,6 +65,10 @@ export class YswgApi {
|
|
|
65
65
|
return this.request("/system/ai-prompt-templates/options", { params: { appId } });
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
amazonProductByAsin(asin, { max = 7 } = {}) {
|
|
69
|
+
return this.request(`/web/amazon/asin/${encodeURIComponent(asin)}`, { params: { max } });
|
|
70
|
+
}
|
|
71
|
+
|
|
68
72
|
createTasks(payload) {
|
|
69
73
|
return this.request("/web/ai/invoke/tasks", { method: "POST", data: payload });
|
|
70
74
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseArgs, readFlag, splitCsv } from "./args.js";
|
|
2
|
-
import { loadConfig, normalizeEmail, resolveConfig, saveConfig, DEFAULT_CONFIG_PATH } from "./config.js";
|
|
2
|
+
import { loadConfig, normalizeEmail, resolveConfig, saveConfig, DEFAULT_APP_ID, DEFAULT_APP_NAME, DEFAULT_CONFIG_PATH } from "./config.js";
|
|
3
|
+
import { loadModelCache, refreshModelCache } from "./model-cache.js";
|
|
3
4
|
import { YswgApi } from "./api.js";
|
|
4
5
|
import {
|
|
5
6
|
buildGeneratePayload,
|
|
@@ -25,8 +26,9 @@ export function buildHelpText() {
|
|
|
25
26
|
Commands:
|
|
26
27
|
auth send-code --email <name|email>
|
|
27
28
|
auth login --email <name|email> --code <6 digits>
|
|
28
|
-
models [--
|
|
29
|
-
templates [--
|
|
29
|
+
models [refresh] [--json]
|
|
30
|
+
templates [--json]
|
|
31
|
+
amazon asin <asin> [--max 7] [--json]
|
|
30
32
|
tasks search [--size 10] [--keyword <text>] [--tab all|expiring1d|expiring2d|nightQueue] [--json]
|
|
31
33
|
tasks get --id <record-id> [--json]
|
|
32
34
|
tasks recover --task-id <invoke-task-id>[,<id>] [--out outputs] [--json]
|
|
@@ -36,13 +38,17 @@ Commands:
|
|
|
36
38
|
|
|
37
39
|
Agent usage:
|
|
38
40
|
Always prefer --json for machine-readable output.
|
|
41
|
+
Default app: ${DEFAULT_APP_NAME} (${DEFAULT_APP_ID}).
|
|
42
|
+
The app ID is fixed; --app-id and YSWG_APP_ID are not supported.
|
|
43
|
+
Model metadata is read from local cache. Use "models refresh --json" after backend model changes.
|
|
39
44
|
Defaults: group-id=6, model=gemini-3.1-flash-image-preview, ratio=1:1, count=1.
|
|
40
45
|
One generate request supports count 1-4. For larger batches, split calls.
|
|
41
46
|
For long tasks, use --no-wait first, then tasks recover --task-id <id>.
|
|
42
47
|
Keep generated image file paths and short summaries in agent context; do not paste image bytes or base64.
|
|
48
|
+
Amazon ASIN gallery lookup: amazon asin <asin> --max 7 --json returns asin, title, images.
|
|
43
49
|
|
|
44
50
|
Environment:
|
|
45
|
-
YSWG_TOKEN, YSWG_REFRESH_TOKEN,
|
|
51
|
+
YSWG_TOKEN, YSWG_REFRESH_TOKEN, YSWG_BASE_URL, YSWG_WS_PATH
|
|
46
52
|
|
|
47
53
|
Config:
|
|
48
54
|
${DEFAULT_CONFIG_PATH}
|
|
@@ -56,6 +62,11 @@ function writeOutput(write, data, json = false) {
|
|
|
56
62
|
write(`${text}\n`);
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
function rejectAppIdOverride(flags) {
|
|
66
|
+
if (flags.appId !== undefined) throw new Error("不允许传入 --app-id;CLI 固定使用 npm app。");
|
|
67
|
+
if (process.env.YSWG_APP_ID) throw new Error("不允许设置 YSWG_APP_ID;CLI 固定使用 npm app。");
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
function assertAuthenticated(resolved) {
|
|
60
71
|
if (!resolved.token) throw new Error("missing token; run auth login or set YSWG_TOKEN");
|
|
61
72
|
}
|
|
@@ -107,7 +118,7 @@ async function handleGenerate({ api, config, flags, resolved, json, write }) {
|
|
|
107
118
|
});
|
|
108
119
|
|
|
109
120
|
if (!flags.skipValidate) {
|
|
110
|
-
const modelGroups = await
|
|
121
|
+
const modelGroups = await loadModelCache();
|
|
111
122
|
validateGenerateOptions({
|
|
112
123
|
model: findModelByGroupId(modelGroups, groupId),
|
|
113
124
|
ratio,
|
|
@@ -178,7 +189,7 @@ async function handleTaskCommand({ api, config, flags, resolved, subcommand, jso
|
|
|
178
189
|
tab: String(readFlag(flags, ["tab"], "all")),
|
|
179
190
|
keyword: String(readFlag(flags, ["keyword", "q"], "")),
|
|
180
191
|
userId: getUserId(config),
|
|
181
|
-
appId:
|
|
192
|
+
appId: resolved.appId,
|
|
182
193
|
size: readFlag(flags, ["size"], 10),
|
|
183
194
|
}));
|
|
184
195
|
writeOutput(write, result, json);
|
|
@@ -208,7 +219,7 @@ async function handleTaskCommand({ api, config, flags, resolved, subcommand, jso
|
|
|
208
219
|
if (!taskIds.length) throw new Error("missing --task-id");
|
|
209
220
|
const records = await findRecordsByInvokeTaskIds(api, {
|
|
210
221
|
taskIds,
|
|
211
|
-
appId:
|
|
222
|
+
appId: resolved.appId,
|
|
212
223
|
userId: getUserId(config),
|
|
213
224
|
pageSize: normalizeHistoryLimit(readFlag(flags, ["size"], 10)),
|
|
214
225
|
maxPages: 1,
|
|
@@ -221,19 +232,43 @@ async function handleTaskCommand({ api, config, flags, resolved, subcommand, jso
|
|
|
221
232
|
}
|
|
222
233
|
}
|
|
223
234
|
|
|
235
|
+
function normalizeAsin(value) {
|
|
236
|
+
const asin = String(value || "").trim().toUpperCase();
|
|
237
|
+
if (!asin) throw new Error("missing asin");
|
|
238
|
+
return asin;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizeAmazonMax(value, fallback = 7) {
|
|
242
|
+
const max = Number(value ?? fallback);
|
|
243
|
+
if (!Number.isFinite(max) || max < 1) throw new Error("max must be a positive number");
|
|
244
|
+
return Math.floor(max);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function handleAmazonCommand({ api, flags, resolved, subcommand, positionals, json, write }) {
|
|
248
|
+
if (subcommand !== "asin") return false;
|
|
249
|
+
assertAuthenticated(resolved);
|
|
250
|
+
const asin = normalizeAsin(readFlag(flags, ["asin"], positionals[0]));
|
|
251
|
+
const max = normalizeAmazonMax(readFlag(flags, ["max"], 7));
|
|
252
|
+
writeOutput(write, await api.amazonProductByAsin(asin, { max }), json);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
224
256
|
export async function runCli(argv, {
|
|
225
257
|
loadConfigFn = loadConfig,
|
|
226
258
|
saveConfigFn = saveConfig,
|
|
259
|
+
loadModelCacheFn = loadModelCache,
|
|
260
|
+
refreshModelCacheFn = refreshModelCache,
|
|
227
261
|
createApi = (resolved) => new YswgApi(resolved),
|
|
228
262
|
write = (text) => process.stdout.write(text),
|
|
229
263
|
} = {}) {
|
|
230
264
|
const parsed = parseArgs(argv);
|
|
231
|
-
const { command, subcommand, flags } = parsed;
|
|
265
|
+
const { command, subcommand, flags, positionals } = parsed;
|
|
232
266
|
const json = Boolean(flags.json);
|
|
233
267
|
if (command === "help" || flags.help) {
|
|
234
268
|
write(buildHelpText());
|
|
235
269
|
return;
|
|
236
270
|
}
|
|
271
|
+
rejectAppIdOverride(flags);
|
|
237
272
|
|
|
238
273
|
const config = await loadConfigFn();
|
|
239
274
|
const resolved = resolveConfig(config, flags);
|
|
@@ -268,16 +303,25 @@ export async function runCli(argv, {
|
|
|
268
303
|
}
|
|
269
304
|
|
|
270
305
|
if (command === "models") {
|
|
271
|
-
|
|
306
|
+
if (subcommand === "refresh") {
|
|
307
|
+
assertAuthenticated(resolved);
|
|
308
|
+
const result = await refreshModelCacheFn({
|
|
309
|
+
fetchModels: () => api.models(resolved.appId),
|
|
310
|
+
});
|
|
311
|
+
writeOutput(write, { ok: true, ...result }, json);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
writeOutput(write, await loadModelCacheFn(), json);
|
|
272
315
|
return;
|
|
273
316
|
}
|
|
274
317
|
|
|
275
318
|
if (command === "templates") {
|
|
276
319
|
assertAuthenticated(resolved);
|
|
277
|
-
writeOutput(write, await api.templates(
|
|
320
|
+
writeOutput(write, await api.templates(resolved.appId), json);
|
|
278
321
|
return;
|
|
279
322
|
}
|
|
280
323
|
|
|
324
|
+
if (command === "amazon" && await handleAmazonCommand({ api, flags, resolved, subcommand, positionals, json, write })) return;
|
|
281
325
|
if (command === "tasks" && await handleTaskCommand({ api, config, flags, resolved, subcommand, json, write })) return;
|
|
282
326
|
if (command === "generate") {
|
|
283
327
|
await handleGenerate({ api, config, flags, resolved, json, write });
|
package/src/config.js
CHANGED
|
@@ -4,7 +4,8 @@ import { homedir } from "node:os";
|
|
|
4
4
|
|
|
5
5
|
export const DEFAULT_CONFIG_PATH = join(homedir(), ".yswg-img-cli", "config.json");
|
|
6
6
|
export const DEFAULT_BASE_URL = "https://www.yswg.love";
|
|
7
|
-
export const
|
|
7
|
+
export const DEFAULT_APP_NAME = "npm";
|
|
8
|
+
export const DEFAULT_APP_ID = "2066371323654864898";
|
|
8
9
|
export const DEFAULT_WS_PATH = "/websocket";
|
|
9
10
|
|
|
10
11
|
export async function loadConfig(path = DEFAULT_CONFIG_PATH) {
|
|
@@ -25,7 +26,7 @@ export async function saveConfig(config, path = DEFAULT_CONFIG_PATH) {
|
|
|
25
26
|
export function resolveConfig(config, flags = {}) {
|
|
26
27
|
return {
|
|
27
28
|
baseUrl: flags.baseUrl || process.env.YSWG_BASE_URL || config.baseUrl || DEFAULT_BASE_URL,
|
|
28
|
-
appId:
|
|
29
|
+
appId: DEFAULT_APP_ID,
|
|
29
30
|
token: flags.token || process.env.YSWG_TOKEN || config.token || "",
|
|
30
31
|
refreshToken: flags.refreshToken || process.env.YSWG_REFRESH_TOKEN || config.refreshToken || "",
|
|
31
32
|
wsPath: flags.wsPath || process.env.YSWG_WS_PATH || config.wsPath || DEFAULT_WS_PATH,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_MODEL_CACHE_PATH = join(homedir(), ".yswg-img-cli", "model-cache.json");
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_MODEL_GROUPS = [
|
|
8
|
+
{
|
|
9
|
+
typeId: "2066371840108666882",
|
|
10
|
+
typeName: "npm",
|
|
11
|
+
typeCode: "npm",
|
|
12
|
+
sort: 0,
|
|
13
|
+
enabled: true,
|
|
14
|
+
bindingType: 1,
|
|
15
|
+
templates: [],
|
|
16
|
+
models: [
|
|
17
|
+
{
|
|
18
|
+
groupId: "2",
|
|
19
|
+
modelName: "gemini-3-pro-image-preview",
|
|
20
|
+
size: "1k",
|
|
21
|
+
label: "天才猴子二代1k",
|
|
22
|
+
points: 8,
|
|
23
|
+
imageGenOptions: { imageCountEnabled: true, imageRadioEnabled: true },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
groupId: "3",
|
|
27
|
+
modelName: "gemini-3-pro-image-preview",
|
|
28
|
+
size: "2k",
|
|
29
|
+
label: "天才猴子二代2k",
|
|
30
|
+
points: 8,
|
|
31
|
+
imageGenOptions: { imageCountEnabled: true, imageRadioEnabled: true },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
groupId: "5",
|
|
35
|
+
modelName: "gemini-3-pro-image-preview",
|
|
36
|
+
size: "4k",
|
|
37
|
+
label: "天才猴子二代4k",
|
|
38
|
+
points: 8,
|
|
39
|
+
imageGenOptions: { imageCountEnabled: true, imageRadioEnabled: true },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
groupId: "1",
|
|
43
|
+
modelName: "gemini-2.5-flash-image",
|
|
44
|
+
size: "1k",
|
|
45
|
+
label: "天才猴子一代",
|
|
46
|
+
points: 1,
|
|
47
|
+
imageGenOptions: { imageCountEnabled: true, imageRadioEnabled: true },
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
groupId: "4",
|
|
51
|
+
modelName: "gemini-3.1-flash-image-preview",
|
|
52
|
+
size: "2k",
|
|
53
|
+
label: "天才猴子三代2k",
|
|
54
|
+
points: 2,
|
|
55
|
+
imageGenOptions: { imageCountEnabled: true, imageRadioEnabled: true },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
groupId: "6",
|
|
59
|
+
modelName: "gemini-3.1-flash-image-preview",
|
|
60
|
+
size: "4k",
|
|
61
|
+
label: "天才猴子三代4k",
|
|
62
|
+
points: 2,
|
|
63
|
+
imageGenOptions: { imageCountEnabled: true, imageRadioEnabled: true },
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
groupId: "2046043157249933314",
|
|
67
|
+
modelName: "图灵二代",
|
|
68
|
+
size: "1k",
|
|
69
|
+
label: "图灵二代",
|
|
70
|
+
points: 2,
|
|
71
|
+
imageGenOptions: { imageCountEnabled: true, imageRadioEnabled: true },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
groupId: "2046853405565095939",
|
|
75
|
+
modelName: "图灵二代(高清)",
|
|
76
|
+
size: "2k",
|
|
77
|
+
label: "图灵二代高清",
|
|
78
|
+
points: 2,
|
|
79
|
+
imageGenOptions: { imageCountEnabled: true, imageRadioEnabled: true },
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
groupId: "2046853405565095940",
|
|
83
|
+
modelName: "图灵二代(超清)",
|
|
84
|
+
size: "4k",
|
|
85
|
+
label: "图灵二代超清",
|
|
86
|
+
points: 2,
|
|
87
|
+
imageGenOptions: { imageCountEnabled: true, imageRadioEnabled: true },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
groupId: "12",
|
|
91
|
+
modelName: "gemini-2.5-flash-lite",
|
|
92
|
+
size: "",
|
|
93
|
+
label: "一代文本",
|
|
94
|
+
points: 1,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
groupId: "13",
|
|
98
|
+
modelName: "gemini-3-flash-preview",
|
|
99
|
+
size: "",
|
|
100
|
+
label: "二代文本",
|
|
101
|
+
points: 1,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
groupId: "930000000000000301",
|
|
105
|
+
modelName: "seedance-2-0",
|
|
106
|
+
size: "720p",
|
|
107
|
+
label: "Seedance 2.0",
|
|
108
|
+
points: 6,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
groupId: "910000000000000304",
|
|
112
|
+
modelName: "kling-3.0-omni",
|
|
113
|
+
size: "720p",
|
|
114
|
+
label: "kling 3.0",
|
|
115
|
+
points: 5,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
groupId: "21",
|
|
119
|
+
modelName: "grok_video",
|
|
120
|
+
size: "720P",
|
|
121
|
+
label: "Grok视频",
|
|
122
|
+
points: 15,
|
|
123
|
+
skipAspectRatio: true,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
groupId: "17",
|
|
127
|
+
modelName: "veo_fast",
|
|
128
|
+
size: "720x1280",
|
|
129
|
+
label: "VEO",
|
|
130
|
+
points: 15,
|
|
131
|
+
skipAspectRatio: true,
|
|
132
|
+
imageGenOptions: { imageCountEnabled: true, imageRadioEnabled: true },
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
export async function loadModelCache(cachePath = DEFAULT_MODEL_CACHE_PATH) {
|
|
139
|
+
try {
|
|
140
|
+
const text = await readFile(cachePath, "utf8");
|
|
141
|
+
const parsed = JSON.parse(text);
|
|
142
|
+
return Array.isArray(parsed?.models) ? parsed.models : parsed;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error.code === "ENOENT") return DEFAULT_MODEL_GROUPS;
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function refreshModelCache({
|
|
150
|
+
cachePath = DEFAULT_MODEL_CACHE_PATH,
|
|
151
|
+
fetchModels,
|
|
152
|
+
} = {}) {
|
|
153
|
+
if (typeof fetchModels !== "function") throw new Error("missing fetchModels");
|
|
154
|
+
const models = await fetchModels();
|
|
155
|
+
const payload = {
|
|
156
|
+
refreshedAt: new Date().toISOString(),
|
|
157
|
+
models,
|
|
158
|
+
};
|
|
159
|
+
await mkdir(dirname(cachePath), { recursive: true });
|
|
160
|
+
await writeFile(cachePath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
161
|
+
return { cachePath, models };
|
|
162
|
+
}
|