@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 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
- 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.
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 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.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yswgaicx/yswg-img-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "CLI wrapper for YSWG Monkey Genius image generation.",
5
5
  "type": "module",
6
6
  "bin": {
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 [--app-id <id>]
29
- templates [--app-id <id>] [--json]
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, YSWG_APP_ID, YSWG_BASE_URL, YSWG_WS_PATH
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 api.models(resolved.appId);
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: String(readFlag(flags, ["appId"], resolved.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: String(readFlag(flags, ["appId"], resolved.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
- 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);
272
315
  return;
273
316
  }
274
317
 
275
318
  if (command === "templates") {
276
319
  assertAuthenticated(resolved);
277
- writeOutput(write, await api.templates(String(readFlag(flags, ["appId"], resolved.appId))), json);
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 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
+ }