@yswgaicx/yswg-img-cli 0.1.5 → 0.1.7

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 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
@@ -204,18 +216,37 @@ The command calls `/prod-api/web/amazon/asin/{asin}` and returns the backend
204
216
 
205
217
  ## Configuration
206
218
 
207
- Flags override environment variables, which override the saved config.
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.
208
222
 
209
223
  - `YSWG_TOKEN`
210
224
  - `YSWG_REFRESH_TOKEN`
211
- - `YSWG_APP_ID`
212
225
  - `YSWG_BASE_URL`
213
226
  - `YSWG_WS_PATH`
214
227
 
215
- Default app ID: `2014153035982319618`.
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.
216
231
  Default ratio: `1:1`.
217
232
  Default generation model: `gemini-3.1-flash-image-preview` (`groupId=6`, 三代).
218
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
+
219
250
  ## Code Structure
220
251
 
221
252
  - `bin/yswg-img.js`: executable entrypoint only.
@@ -223,5 +254,6 @@ Default generation model: `gemini-3.1-flash-image-preview` (`groupId=6`, 三代)
223
254
  - `src/generate.js`: generation payloads, validation, history recovery, WebSocket wait, uploads, downloads.
224
255
  - `src/api.js`: YSWG HTTP API client.
225
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.
226
258
  - `src/args.js`: minimal CLI argument parsing.
227
259
  - `src/image-compress.js`: frontend-compatible reference image compression.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yswgaicx/yswg-img-cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "CLI wrapper for YSWG Monkey Genius image generation.",
5
5
  "type": "module",
6
6
  "bin": {
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,8 @@ 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 [--app-id <id>]
29
- templates [--app-id <id>] [--json]
29
+ models [refresh] [--json]
30
+ templates [--json]
30
31
  amazon asin <asin> [--max 7] [--json]
31
32
  tasks search [--size 10] [--keyword <text>] [--tab all|expiring1d|expiring2d|nightQueue] [--json]
32
33
  tasks get --id <record-id> [--json]
@@ -37,6 +38,9 @@ Commands:
37
38
 
38
39
  Agent usage:
39
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.
40
44
  Defaults: group-id=6, model=gemini-3.1-flash-image-preview, ratio=1:1, count=1.
41
45
  One generate request supports count 1-4. For larger batches, split calls.
42
46
  For long tasks, use --no-wait first, then tasks recover --task-id <id>.
@@ -44,7 +48,7 @@ Agent usage:
44
48
  Amazon ASIN gallery lookup: amazon asin <asin> --max 7 --json returns asin, title, images.
45
49
 
46
50
  Environment:
47
- YSWG_TOKEN, YSWG_REFRESH_TOKEN, YSWG_APP_ID, YSWG_BASE_URL, YSWG_WS_PATH
51
+ YSWG_TOKEN, YSWG_REFRESH_TOKEN, YSWG_BASE_URL, YSWG_WS_PATH
48
52
 
49
53
  Config:
50
54
  ${DEFAULT_CONFIG_PATH}
@@ -58,6 +62,11 @@ function writeOutput(write, data, json = false) {
58
62
  write(`${text}\n`);
59
63
  }
60
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
+
61
70
  function assertAuthenticated(resolved) {
62
71
  if (!resolved.token) throw new Error("missing token; run auth login or set YSWG_TOKEN");
63
72
  }
@@ -109,7 +118,7 @@ async function handleGenerate({ api, config, flags, resolved, json, write }) {
109
118
  });
110
119
 
111
120
  if (!flags.skipValidate) {
112
- const modelGroups = await api.models(resolved.appId);
121
+ const modelGroups = await loadModelCache();
113
122
  validateGenerateOptions({
114
123
  model: findModelByGroupId(modelGroups, groupId),
115
124
  ratio,
@@ -180,7 +189,7 @@ async function handleTaskCommand({ api, config, flags, resolved, subcommand, jso
180
189
  tab: String(readFlag(flags, ["tab"], "all")),
181
190
  keyword: String(readFlag(flags, ["keyword", "q"], "")),
182
191
  userId: getUserId(config),
183
- appId: String(readFlag(flags, ["appId"], resolved.appId)),
192
+ appId: resolved.appId,
184
193
  size: readFlag(flags, ["size"], 10),
185
194
  }));
186
195
  writeOutput(write, result, json);
@@ -210,7 +219,7 @@ async function handleTaskCommand({ api, config, flags, resolved, subcommand, jso
210
219
  if (!taskIds.length) throw new Error("missing --task-id");
211
220
  const records = await findRecordsByInvokeTaskIds(api, {
212
221
  taskIds,
213
- appId: String(readFlag(flags, ["appId"], resolved.appId)),
222
+ appId: resolved.appId,
214
223
  userId: getUserId(config),
215
224
  pageSize: normalizeHistoryLimit(readFlag(flags, ["size"], 10)),
216
225
  maxPages: 1,
@@ -247,6 +256,8 @@ async function handleAmazonCommand({ api, flags, resolved, subcommand, positiona
247
256
  export async function runCli(argv, {
248
257
  loadConfigFn = loadConfig,
249
258
  saveConfigFn = saveConfig,
259
+ loadModelCacheFn = loadModelCache,
260
+ refreshModelCacheFn = refreshModelCache,
250
261
  createApi = (resolved) => new YswgApi(resolved),
251
262
  write = (text) => process.stdout.write(text),
252
263
  } = {}) {
@@ -257,6 +268,7 @@ export async function runCli(argv, {
257
268
  write(buildHelpText());
258
269
  return;
259
270
  }
271
+ rejectAppIdOverride(flags);
260
272
 
261
273
  const config = await loadConfigFn();
262
274
  const resolved = resolveConfig(config, flags);
@@ -291,13 +303,21 @@ export async function runCli(argv, {
291
303
  }
292
304
 
293
305
  if (command === "models") {
294
- writeOutput(write, await api.models(resolved.appId), json);
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);
295
315
  return;
296
316
  }
297
317
 
298
318
  if (command === "templates") {
299
319
  assertAuthenticated(resolved);
300
- writeOutput(write, await api.templates(String(readFlag(flags, ["appId"], resolved.appId))), json);
320
+ writeOutput(write, await api.templates(resolved.appId), json);
301
321
  return;
302
322
  }
303
323
 
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 DEFAULT_APP_ID = "2014153035982319618";
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: flags.appId || process.env.YSWG_APP_ID || config.appId || DEFAULT_APP_ID,
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
+ }