@yswgaicx/yswg-img-cli 0.1.3 → 0.1.5

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
@@ -30,16 +30,54 @@ yswg-img auth login --email zhangsan --code 123456
30
30
 
31
31
  The token is saved to `~/.yswg-img-cli/config.json`.
32
32
 
33
+ ## Agent Usage
34
+
35
+ Use these rules when an AI agent calls the CLI:
36
+
37
+ - Prefer `--json` for every command so downstream tools can parse stable output.
38
+ - Use defaults unless the user explicitly asks otherwise: `groupId=6`, model
39
+ `gemini-3.1-flash-image-preview`, ratio `1:1`, count `1`.
40
+ - For one-shot generation, call:
41
+
42
+ ```bash
43
+ yswg-img generate --prompt "一只可爱的白色兔子,粉色背景" --json
44
+ ```
45
+
46
+ - For up to 4 parallel images in one backend request, call:
47
+
48
+ ```bash
49
+ yswg-img generate --prompt "四张电商产品场景图" --count 4 --json
50
+ ```
51
+
52
+ - For long-running jobs, avoid duplicate submissions:
53
+
54
+ ```bash
55
+ yswg-img generate --prompt "复杂场景图" --no-wait --json
56
+ yswg-img tasks recover --task-id <task-id-from-json> --out outputs/recovered --json
57
+ ```
58
+
59
+ - If sync waiting times out, run `tasks recover` with the returned task ID before
60
+ submitting the same prompt again.
61
+ - For generated images, keep only local file paths and short summaries in agent
62
+ context. Do not paste image bytes, base64, `data:image/...`, or full tool
63
+ output into the conversation.
64
+ - To fetch an Amazon product gallery by ASIN, call:
65
+
66
+ ```bash
67
+ yswg-img amazon asin B0TEST1234 --max 7 --json
68
+ ```
69
+
33
70
  ## Generate
34
71
 
35
72
  Supported Monkey Genius options mirrored by the CLI:
36
73
 
37
74
  - Model/group selection: `--group-id`, `--model`
38
75
  - Aspect ratio: `--ratio 1:1|3:2|2:3|4:3|3:4|16:9|9:16|5:4|4:5|21:9`
39
- - Single/double image output: `--count 1` or `--count 2` when the selected model supports it
76
+ - Concurrent image output: `--count 1` through `--count 4` when the selected model supports it
40
77
  - Reference images: `--ref ./a.png,https://example.com/b.jpg`
41
78
  - Night/off-peak queue submission: `--night`
42
79
  - AI tool/template payloads: `--template-code`, `--template-vars-json`
80
+ - Amazon ASIN product gallery lookup: `amazon asin <asin> --max 7`
43
81
  - Generation history lookup and timeout recovery: `tasks search`, `tasks recover`
44
82
  - History tabs/search/delete/night-cancel: `tasks search --tab ...`, `tasks delete`, `tasks cancel-night`
45
83
 
@@ -71,11 +109,19 @@ Submit and wait for output images:
71
109
  yswg-img generate \
72
110
  --prompt "一张白底产品图,柔和自然光,高级电商摄影" \
73
111
  --ratio 1:1 \
74
- --count 1 \
112
+ --count 4 \
75
113
  --out outputs \
76
114
  --json
77
115
  ```
78
116
 
117
+ The CLI caps one generation request at 4 images. When the backend returns
118
+ multiple task IDs, the CLI waits for those tasks concurrently and downloads the
119
+ result files by path.
120
+
121
+ For image-heavy workflows, keep local file paths and short summaries in agent
122
+ context. Do not paste high-resolution image data, base64, `data:image/...`, or
123
+ full tool output into the conversation.
124
+
79
125
  Use reference images:
80
126
 
81
127
  ```bash
@@ -147,6 +193,15 @@ yswg-img tasks delete --id <record-id> --json
147
193
  yswg-img tasks cancel-night --id <record-id> --json
148
194
  ```
149
195
 
196
+ Fetch Amazon product title and main image gallery by ASIN:
197
+
198
+ ```bash
199
+ yswg-img amazon asin B0TEST1234 --max 7 --json
200
+ ```
201
+
202
+ The command calls `/prod-api/web/amazon/asin/{asin}` and returns the backend
203
+ `AmazonProductVo` shape: `asin`, `title`, and `images`.
204
+
150
205
  ## Configuration
151
206
 
152
207
  Flags override environment variables, which override the saved config.
@@ -160,3 +215,13 @@ Flags override environment variables, which override the saved config.
160
215
  Default app ID: `2014153035982319618`.
161
216
  Default ratio: `1:1`.
162
217
  Default generation model: `gemini-3.1-flash-image-preview` (`groupId=6`, 三代).
218
+
219
+ ## Code Structure
220
+
221
+ - `bin/yswg-img.js`: executable entrypoint only.
222
+ - `src/cli.js`: command dispatch, dependency injection, JSON output, and agent help text.
223
+ - `src/generate.js`: generation payloads, validation, history recovery, WebSocket wait, uploads, downloads.
224
+ - `src/api.js`: YSWG HTTP API client.
225
+ - `src/config.js`: config loading, env/flag precedence, email normalization.
226
+ - `src/args.js`: minimal CLI argument parsing.
227
+ - `src/image-compress.js`: frontend-compatible reference image compression.
package/bin/yswg-img.js CHANGED
@@ -1,258 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { parseArgs, readFlag, splitCsv } from "../src/args.js";
3
- import { loadConfig, normalizeEmail, resolveConfig, saveConfig, DEFAULT_CONFIG_PATH } from "../src/config.js";
4
- import { YswgApi } from "../src/api.js";
5
- import {
6
- buildGeneratePayload,
7
- DEFAULT_GENERATE_OPTIONS,
8
- downloadImages,
9
- extractImageUrls,
10
- findModelByGroupId,
11
- findRecordsByInvokeTaskIds,
12
- isTimeoutLikeError,
13
- normalizeHistoryLimit,
14
- normalizeTaskSearchParams,
15
- parseTemplateInput,
16
- taskImageUrlsFromRecord,
17
- uploadReferences,
18
- validateGenerateOptions,
19
- waitForTask,
20
- } from "../src/generate.js";
2
+ import { runCli } from "../src/cli.js";
21
3
 
22
- function printHelp() {
23
- console.log(`yswg-img
24
-
25
- Commands:
26
- auth send-code --email <name|email>
27
- auth login --email <name|email> --code <6 digits>
28
- models [--app-id <id>]
29
- templates [--app-id <id>] [--json]
30
- tasks search [--size 10] [--keyword <text>] [--tab all|expiring1d|expiring2d|nightQueue] [--json]
31
- tasks get --id <record-id> [--json]
32
- tasks recover --task-id <invoke-task-id>[,<id>] [--out outputs] [--json]
33
- tasks delete --id <record-id> [--json]
34
- tasks cancel-night --id <record-id> [--json]
35
- generate --prompt <text> [--group-id 6] [--model gemini-3.1-flash-image-preview] [--ratio 1:1] [--count 1] [--ref a.png,b.jpg] [--template-code <code>] [--template-vars-json '{}'] [--no-compress] [--out outputs]
36
-
37
- Environment:
38
- YSWG_TOKEN, YSWG_REFRESH_TOKEN, YSWG_APP_ID, YSWG_BASE_URL, YSWG_WS_PATH
39
-
40
- Config:
41
- ${DEFAULT_CONFIG_PATH}
42
- `);
43
- }
44
-
45
- function output(data, json = false) {
46
- if (json) console.log(JSON.stringify(data, null, 2));
47
- else if (typeof data === "string") console.log(data);
48
- else console.log(JSON.stringify(data, null, 2));
49
- }
50
-
51
- async function main() {
52
- const parsed = parseArgs(process.argv.slice(2));
53
- const { command, subcommand, flags } = parsed;
54
- const json = Boolean(flags.json);
55
- if (command === "help" || flags.help) return printHelp();
56
-
57
- const config = await loadConfig();
58
- const resolved = resolveConfig(config, flags);
59
- const api = new YswgApi(resolved);
60
-
61
- if (command === "auth" && subcommand === "send-code") {
62
- const email = normalizeEmail(readFlag(flags, ["email", "e"]));
63
- const check = await api.checkEmail(email);
64
- if (check?.registered === false) throw new Error(check.message || "email is not registered");
65
- await api.sendCode(email);
66
- output({ ok: true, email, message: "验证码已发送,请在企业微信中查收" }, json);
67
- return;
68
- }
69
-
70
- if (command === "auth" && subcommand === "login") {
71
- const email = normalizeEmail(readFlag(flags, ["email", "e"]));
72
- const code = String(readFlag(flags, ["code", "c"], "")).trim();
73
- if (!code) throw new Error("missing --code");
74
- const session = await api.login(email, code);
75
- const next = {
76
- ...config,
77
- baseUrl: resolved.baseUrl,
78
- appId: resolved.appId,
79
- wsPath: resolved.wsPath,
80
- token: session.token,
81
- refreshToken: session.refreshToken,
82
- user: session,
83
- email,
84
- };
85
- await saveConfig(next);
86
- output({ ok: true, email, configPath: DEFAULT_CONFIG_PATH }, json);
87
- return;
88
- }
89
-
90
- if (command === "models") {
91
- const models = await api.models(resolved.appId);
92
- output(models, json);
93
- return;
94
- }
95
-
96
- if (command === "templates") {
97
- if (!resolved.token) throw new Error("missing token; run auth login or set YSWG_TOKEN");
98
- const templates = await api.templates(String(readFlag(flags, ["appId"], resolved.appId)));
99
- output(templates, json);
100
- return;
101
- }
102
-
103
- if (command === "tasks" && subcommand === "search") {
104
- if (!resolved.token) throw new Error("missing token; run auth login or set YSWG_TOKEN");
105
- const result = await api.searchGenTasks(normalizeTaskSearchParams({
106
- tab: String(readFlag(flags, ["tab"], "all")),
107
- keyword: String(readFlag(flags, ["keyword", "q"], "")),
108
- userId: config.user?.userId || config.user?.id,
109
- appId: String(readFlag(flags, ["appId"], resolved.appId)),
110
- size: readFlag(flags, ["size"], 10),
111
- }));
112
- output(result, json);
113
- return;
114
- }
115
-
116
- if (command === "tasks" && subcommand === "get") {
117
- if (!resolved.token) throw new Error("missing token; run auth login or set YSWG_TOKEN");
118
- const id = String(readFlag(flags, ["id"], "")).trim();
119
- if (!id) throw new Error("missing --id");
120
- const record = await api.genTask(id);
121
- output(record, json);
122
- return;
123
- }
124
-
125
- if (command === "tasks" && subcommand === "delete") {
126
- if (!resolved.token) throw new Error("missing token; run auth login or set YSWG_TOKEN");
127
- const id = String(readFlag(flags, ["id"], "")).trim();
128
- if (!id) throw new Error("missing --id");
129
- await api.deleteGenTask(id);
130
- output({ ok: true, id }, json);
131
- return;
132
- }
133
-
134
- if (command === "tasks" && subcommand === "cancel-night") {
135
- if (!resolved.token) throw new Error("missing token; run auth login or set YSWG_TOKEN");
136
- const id = String(readFlag(flags, ["id"], "")).trim();
137
- if (!id) throw new Error("missing --id");
138
- await api.cancelNightTask(id);
139
- output({ ok: true, id }, json);
140
- return;
141
- }
142
-
143
- if (command === "tasks" && subcommand === "recover") {
144
- if (!resolved.token) throw new Error("missing token; run auth login or set YSWG_TOKEN");
145
- const taskIds = splitCsv(readFlag(flags, ["taskId", "taskIds", "id"], ""));
146
- if (!taskIds.length) throw new Error("missing --task-id");
147
- const records = await findRecordsByInvokeTaskIds(api, {
148
- taskIds,
149
- appId: String(readFlag(flags, ["appId"], resolved.appId)),
150
- userId: config.user?.userId || config.user?.id,
151
- pageSize: normalizeHistoryLimit(readFlag(flags, ["size"], 10)),
152
- maxPages: 1,
153
- });
154
- const imageUrls = records.flatMap((record) => taskImageUrlsFromRecord(record));
155
- if (!imageUrls.length) throw new Error("最近 10 条生成记录里还没有找到该任务结果;如果需要查看更多,请去导航站「天才猴子」页面查询。");
156
- const files = flags.out === "none" ? [] : await downloadImages(imageUrls, String(readFlag(flags, ["out"], "outputs")));
157
- output({ ok: true, taskIds, recordIds: records.map((record) => record.id), imageUrls, files }, json);
158
- return;
159
- }
160
-
161
- if (command === "generate") {
162
- if (!resolved.token) throw new Error("missing token; run auth login or set YSWG_TOKEN");
163
- const prompt = String(readFlag(flags, ["prompt", "p"], "")).trim();
164
- const refs = splitCsv(readFlag(flags, ["ref", "refs"], ""));
165
- const refImages = await uploadReferences(api, refs, { compress: !flags.noCompress });
166
- const groupId = String(readFlag(flags, ["groupId", "group"], DEFAULT_GENERATE_OPTIONS.groupId));
167
- const model = String(readFlag(flags, ["model", "m"], DEFAULT_GENERATE_OPTIONS.model));
168
- const ratio = String(readFlag(flags, ["ratio", "r"], DEFAULT_GENERATE_OPTIONS.ratio));
169
- const imageCount = Number(readFlag(flags, ["count", "imageCount"], DEFAULT_GENERATE_OPTIONS.imageCount));
170
- const templateInput = parseTemplateInput({
171
- code: String(readFlag(flags, ["templateCode", "template"], "")),
172
- varsJson: String(readFlag(flags, ["templateVarsJson", "varsJson"], "")),
173
- displayPrompt: String(readFlag(flags, ["displayPrompt"], "")),
174
- });
175
-
176
- if (!flags.skipValidate) {
177
- const modelGroups = await api.models(resolved.appId);
178
- validateGenerateOptions({
179
- model: findModelByGroupId(modelGroups, groupId),
180
- ratio,
181
- imageCount,
182
- });
183
- }
184
-
185
- const payload = buildGeneratePayload({
186
- appId: resolved.appId,
187
- groupId,
188
- prompt,
189
- model,
190
- ratio,
191
- imageCount,
192
- refImages,
193
- size: String(readFlag(flags, ["size"], "")),
194
- night: Boolean(flags.night),
195
- templates: templateInput.templates,
196
- displayPrompt: templateInput.displayPrompt,
197
- });
198
-
199
- if (flags.dryRun) {
200
- output({ payload }, json);
201
- return;
202
- }
203
-
204
- const taskIds = await api.createTasks(payload);
205
- const ids = Array.isArray(taskIds) ? taskIds : [taskIds?.taskId || taskIds?.id].filter(Boolean);
206
- if (!ids.length) throw new Error("create task did not return task ids");
207
-
208
- if (flags.noWait || flags.night) {
209
- output({ ok: true, taskIds: ids, payload }, json);
210
- return;
211
- }
212
-
213
- const timeoutMs = Number(readFlag(flags, ["timeoutMs"], Number(readFlag(flags, ["timeout"], 3600)) * 1000));
214
- let imageUrls = [];
215
- let waitError;
216
- try {
217
- const messages = await Promise.all(ids.map((taskId) => waitForTask({
218
- baseUrl: resolved.baseUrl,
219
- token: resolved.token,
220
- wsPath: resolved.wsPath,
221
- taskId,
222
- timeoutMs,
223
- })));
224
- imageUrls = messages.flatMap((message) => extractImageUrls(message.responseJson));
225
- } catch (error) {
226
- if (!isTimeoutLikeError(error)) throw error;
227
- waitError = error;
228
- }
229
-
230
- if (!imageUrls.length) {
231
- const records = await findRecordsByInvokeTaskIds(api, {
232
- taskIds: ids,
233
- appId: resolved.appId,
234
- userId: config.user?.userId || config.user?.id,
235
- pageSize: 10,
236
- maxPages: 1,
237
- });
238
- imageUrls = records.flatMap((record) => taskImageUrlsFromRecord(record));
239
- }
240
-
241
- if (!imageUrls.length && waitError) {
242
- const error = new Error(`${waitError.message}; 最近 10 条生成记录里还没有找到该任务结果。请稍后用 tasks recover --task-id <id> 查询;如果需要查看更多,请去导航站「天才猴子」页面查询,避免重复提交同一任务。`);
243
- error.cause = waitError;
244
- throw error;
245
- }
246
-
247
- const files = flags.out === "none" ? [] : await downloadImages(imageUrls, String(readFlag(flags, ["out"], "outputs")));
248
- output({ ok: true, taskIds: ids, imageUrls, files }, json);
249
- return;
250
- }
251
-
252
- printHelp();
253
- }
254
-
255
- main().catch((error) => {
4
+ runCli(process.argv.slice(2)).catch((error) => {
256
5
  console.error(`yswg-img: ${error.message}`);
257
6
  process.exitCode = 1;
258
7
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yswgaicx/yswg-img-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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 ADDED
@@ -0,0 +1,312 @@
1
+ import { parseArgs, readFlag, splitCsv } from "./args.js";
2
+ import { loadConfig, normalizeEmail, resolveConfig, saveConfig, DEFAULT_CONFIG_PATH } from "./config.js";
3
+ import { YswgApi } from "./api.js";
4
+ import {
5
+ buildGeneratePayload,
6
+ DEFAULT_GENERATE_OPTIONS,
7
+ downloadImages,
8
+ extractImageUrls,
9
+ findModelByGroupId,
10
+ findRecordsByInvokeTaskIds,
11
+ isTimeoutLikeError,
12
+ normalizeImageCount,
13
+ normalizeHistoryLimit,
14
+ normalizeTaskSearchParams,
15
+ parseTemplateInput,
16
+ taskImageUrlsFromRecord,
17
+ uploadReferences,
18
+ validateGenerateOptions,
19
+ waitForTask,
20
+ } from "./generate.js";
21
+
22
+ export function buildHelpText() {
23
+ return `yswg-img
24
+
25
+ Commands:
26
+ auth send-code --email <name|email>
27
+ auth login --email <name|email> --code <6 digits>
28
+ models [--app-id <id>]
29
+ templates [--app-id <id>] [--json]
30
+ amazon asin <asin> [--max 7] [--json]
31
+ tasks search [--size 10] [--keyword <text>] [--tab all|expiring1d|expiring2d|nightQueue] [--json]
32
+ tasks get --id <record-id> [--json]
33
+ tasks recover --task-id <invoke-task-id>[,<id>] [--out outputs] [--json]
34
+ tasks delete --id <record-id> [--json]
35
+ tasks cancel-night --id <record-id> [--json]
36
+ generate --prompt <text> [--group-id 6] [--model gemini-3.1-flash-image-preview] [--ratio 1:1] [--count 1-4] [--ref a.png,b.jpg] [--template-code <code>] [--template-vars-json '{}'] [--no-compress] [--out outputs]
37
+
38
+ Agent usage:
39
+ Always prefer --json for machine-readable output.
40
+ Defaults: group-id=6, model=gemini-3.1-flash-image-preview, ratio=1:1, count=1.
41
+ One generate request supports count 1-4. For larger batches, split calls.
42
+ For long tasks, use --no-wait first, then tasks recover --task-id <id>.
43
+ Keep generated image file paths and short summaries in agent context; do not paste image bytes or base64.
44
+ Amazon ASIN gallery lookup: amazon asin <asin> --max 7 --json returns asin, title, images.
45
+
46
+ Environment:
47
+ YSWG_TOKEN, YSWG_REFRESH_TOKEN, YSWG_APP_ID, YSWG_BASE_URL, YSWG_WS_PATH
48
+
49
+ Config:
50
+ ${DEFAULT_CONFIG_PATH}
51
+ `;
52
+ }
53
+
54
+ function writeOutput(write, data, json = false) {
55
+ const text = json || typeof data !== "string"
56
+ ? JSON.stringify(data, null, 2)
57
+ : data;
58
+ write(`${text}\n`);
59
+ }
60
+
61
+ function assertAuthenticated(resolved) {
62
+ if (!resolved.token) throw new Error("missing token; run auth login or set YSWG_TOKEN");
63
+ }
64
+
65
+ function getUserId(config) {
66
+ return config.user?.userId || config.user?.id;
67
+ }
68
+
69
+ function normalizeTaskIds(taskIds) {
70
+ if (Array.isArray(taskIds)) return taskIds;
71
+ return [taskIds?.taskId || taskIds?.id].filter(Boolean);
72
+ }
73
+
74
+ async function recoverImageUrlsFromHistory(api, { taskIds, appId, userId }) {
75
+ const records = await findRecordsByInvokeTaskIds(api, {
76
+ taskIds,
77
+ appId,
78
+ userId,
79
+ pageSize: 10,
80
+ maxPages: 1,
81
+ });
82
+ return records.flatMap((record) => taskImageUrlsFromRecord(record));
83
+ }
84
+
85
+ async function waitForImageUrls({ ids, resolved, timeoutMs }) {
86
+ const messages = await Promise.all(ids.map((taskId) => waitForTask({
87
+ baseUrl: resolved.baseUrl,
88
+ token: resolved.token,
89
+ wsPath: resolved.wsPath,
90
+ taskId,
91
+ timeoutMs,
92
+ })));
93
+ return messages.flatMap((message) => extractImageUrls(message.responseJson));
94
+ }
95
+
96
+ async function handleGenerate({ api, config, flags, resolved, json, write }) {
97
+ assertAuthenticated(resolved);
98
+ const prompt = String(readFlag(flags, ["prompt", "p"], "")).trim();
99
+ const refs = splitCsv(readFlag(flags, ["ref", "refs"], ""));
100
+ const refImages = await uploadReferences(api, refs, { compress: !flags.noCompress });
101
+ const groupId = String(readFlag(flags, ["groupId", "group"], DEFAULT_GENERATE_OPTIONS.groupId));
102
+ const model = String(readFlag(flags, ["model", "m"], DEFAULT_GENERATE_OPTIONS.model));
103
+ const ratio = String(readFlag(flags, ["ratio", "r"], DEFAULT_GENERATE_OPTIONS.ratio));
104
+ const imageCount = normalizeImageCount(readFlag(flags, ["count", "imageCount"], DEFAULT_GENERATE_OPTIONS.imageCount));
105
+ const templateInput = parseTemplateInput({
106
+ code: String(readFlag(flags, ["templateCode", "template"], "")),
107
+ varsJson: String(readFlag(flags, ["templateVarsJson", "varsJson"], "")),
108
+ displayPrompt: String(readFlag(flags, ["displayPrompt"], "")),
109
+ });
110
+
111
+ if (!flags.skipValidate) {
112
+ const modelGroups = await api.models(resolved.appId);
113
+ validateGenerateOptions({
114
+ model: findModelByGroupId(modelGroups, groupId),
115
+ ratio,
116
+ imageCount,
117
+ });
118
+ }
119
+
120
+ const payload = buildGeneratePayload({
121
+ appId: resolved.appId,
122
+ groupId,
123
+ prompt,
124
+ model,
125
+ ratio,
126
+ imageCount,
127
+ refImages,
128
+ size: String(readFlag(flags, ["size"], "")),
129
+ night: Boolean(flags.night),
130
+ templates: templateInput.templates,
131
+ displayPrompt: templateInput.displayPrompt,
132
+ });
133
+
134
+ if (flags.dryRun) {
135
+ writeOutput(write, { payload }, json);
136
+ return;
137
+ }
138
+
139
+ const ids = normalizeTaskIds(await api.createTasks(payload));
140
+ if (!ids.length) throw new Error("create task did not return task ids");
141
+
142
+ if (flags.noWait || flags.night) {
143
+ writeOutput(write, { ok: true, taskIds: ids, payload }, json);
144
+ return;
145
+ }
146
+
147
+ const timeoutMs = Number(readFlag(flags, ["timeoutMs"], Number(readFlag(flags, ["timeout"], 3600)) * 1000));
148
+ let imageUrls = [];
149
+ let waitError;
150
+ try {
151
+ imageUrls = await waitForImageUrls({ ids, resolved, timeoutMs });
152
+ } catch (error) {
153
+ if (!isTimeoutLikeError(error)) throw error;
154
+ waitError = error;
155
+ }
156
+
157
+ if (!imageUrls.length) {
158
+ imageUrls = await recoverImageUrlsFromHistory(api, {
159
+ taskIds: ids,
160
+ appId: resolved.appId,
161
+ userId: getUserId(config),
162
+ });
163
+ }
164
+
165
+ if (!imageUrls.length && waitError) {
166
+ const error = new Error(`${waitError.message}; 最近 10 条生成记录里还没有找到该任务结果。请稍后用 tasks recover --task-id <id> 查询;如果需要查看更多,请去导航站「天才猴子」页面查询,避免重复提交同一任务。`);
167
+ error.cause = waitError;
168
+ throw error;
169
+ }
170
+
171
+ const files = flags.out === "none" ? [] : await downloadImages(imageUrls, String(readFlag(flags, ["out"], "outputs")));
172
+ writeOutput(write, { ok: true, taskIds: ids, imageUrls, files }, json);
173
+ }
174
+
175
+ async function handleTaskCommand({ api, config, flags, resolved, subcommand, json, write }) {
176
+ if (!["search", "get", "delete", "cancel-night", "recover"].includes(subcommand)) return false;
177
+ assertAuthenticated(resolved);
178
+ if (subcommand === "search") {
179
+ const result = await api.searchGenTasks(normalizeTaskSearchParams({
180
+ tab: String(readFlag(flags, ["tab"], "all")),
181
+ keyword: String(readFlag(flags, ["keyword", "q"], "")),
182
+ userId: getUserId(config),
183
+ appId: String(readFlag(flags, ["appId"], resolved.appId)),
184
+ size: readFlag(flags, ["size"], 10),
185
+ }));
186
+ writeOutput(write, result, json);
187
+ return true;
188
+ }
189
+
190
+ const id = String(readFlag(flags, ["id"], "")).trim();
191
+ if (subcommand === "get") {
192
+ if (!id) throw new Error("missing --id");
193
+ writeOutput(write, await api.genTask(id), json);
194
+ return true;
195
+ }
196
+ if (subcommand === "delete") {
197
+ if (!id) throw new Error("missing --id");
198
+ await api.deleteGenTask(id);
199
+ writeOutput(write, { ok: true, id }, json);
200
+ return true;
201
+ }
202
+ if (subcommand === "cancel-night") {
203
+ if (!id) throw new Error("missing --id");
204
+ await api.cancelNightTask(id);
205
+ writeOutput(write, { ok: true, id }, json);
206
+ return true;
207
+ }
208
+ if (subcommand === "recover") {
209
+ const taskIds = splitCsv(readFlag(flags, ["taskId", "taskIds", "id"], ""));
210
+ if (!taskIds.length) throw new Error("missing --task-id");
211
+ const records = await findRecordsByInvokeTaskIds(api, {
212
+ taskIds,
213
+ appId: String(readFlag(flags, ["appId"], resolved.appId)),
214
+ userId: getUserId(config),
215
+ pageSize: normalizeHistoryLimit(readFlag(flags, ["size"], 10)),
216
+ maxPages: 1,
217
+ });
218
+ const imageUrls = records.flatMap((record) => taskImageUrlsFromRecord(record));
219
+ if (!imageUrls.length) throw new Error("最近 10 条生成记录里还没有找到该任务结果;如果需要查看更多,请去导航站「天才猴子」页面查询。");
220
+ const files = flags.out === "none" ? [] : await downloadImages(imageUrls, String(readFlag(flags, ["out"], "outputs")));
221
+ writeOutput(write, { ok: true, taskIds, recordIds: records.map((record) => record.id), imageUrls, files }, json);
222
+ return true;
223
+ }
224
+ }
225
+
226
+ function normalizeAsin(value) {
227
+ const asin = String(value || "").trim().toUpperCase();
228
+ if (!asin) throw new Error("missing asin");
229
+ return asin;
230
+ }
231
+
232
+ function normalizeAmazonMax(value, fallback = 7) {
233
+ const max = Number(value ?? fallback);
234
+ if (!Number.isFinite(max) || max < 1) throw new Error("max must be a positive number");
235
+ return Math.floor(max);
236
+ }
237
+
238
+ async function handleAmazonCommand({ api, flags, resolved, subcommand, positionals, json, write }) {
239
+ if (subcommand !== "asin") return false;
240
+ assertAuthenticated(resolved);
241
+ const asin = normalizeAsin(readFlag(flags, ["asin"], positionals[0]));
242
+ const max = normalizeAmazonMax(readFlag(flags, ["max"], 7));
243
+ writeOutput(write, await api.amazonProductByAsin(asin, { max }), json);
244
+ return true;
245
+ }
246
+
247
+ export async function runCli(argv, {
248
+ loadConfigFn = loadConfig,
249
+ saveConfigFn = saveConfig,
250
+ createApi = (resolved) => new YswgApi(resolved),
251
+ write = (text) => process.stdout.write(text),
252
+ } = {}) {
253
+ const parsed = parseArgs(argv);
254
+ const { command, subcommand, flags, positionals } = parsed;
255
+ const json = Boolean(flags.json);
256
+ if (command === "help" || flags.help) {
257
+ write(buildHelpText());
258
+ return;
259
+ }
260
+
261
+ const config = await loadConfigFn();
262
+ const resolved = resolveConfig(config, flags);
263
+ const api = createApi(resolved);
264
+
265
+ if (command === "auth" && subcommand === "send-code") {
266
+ const email = normalizeEmail(readFlag(flags, ["email", "e"]));
267
+ const check = await api.checkEmail(email);
268
+ if (check?.registered === false) throw new Error(check.message || "email is not registered");
269
+ await api.sendCode(email);
270
+ writeOutput(write, { ok: true, email, message: "验证码已发送,请在企业微信中查收" }, json);
271
+ return;
272
+ }
273
+
274
+ if (command === "auth" && subcommand === "login") {
275
+ const email = normalizeEmail(readFlag(flags, ["email", "e"]));
276
+ const code = String(readFlag(flags, ["code", "c"], "")).trim();
277
+ if (!code) throw new Error("missing --code");
278
+ const session = await api.login(email, code);
279
+ await saveConfigFn({
280
+ ...config,
281
+ baseUrl: resolved.baseUrl,
282
+ appId: resolved.appId,
283
+ wsPath: resolved.wsPath,
284
+ token: session.token,
285
+ refreshToken: session.refreshToken,
286
+ user: session,
287
+ email,
288
+ });
289
+ writeOutput(write, { ok: true, email, configPath: DEFAULT_CONFIG_PATH }, json);
290
+ return;
291
+ }
292
+
293
+ if (command === "models") {
294
+ writeOutput(write, await api.models(resolved.appId), json);
295
+ return;
296
+ }
297
+
298
+ if (command === "templates") {
299
+ assertAuthenticated(resolved);
300
+ writeOutput(write, await api.templates(String(readFlag(flags, ["appId"], resolved.appId))), json);
301
+ return;
302
+ }
303
+
304
+ if (command === "amazon" && await handleAmazonCommand({ api, flags, resolved, subcommand, positionals, json, write })) return;
305
+ if (command === "tasks" && await handleTaskCommand({ api, config, flags, resolved, subcommand, json, write })) return;
306
+ if (command === "generate") {
307
+ await handleGenerate({ api, config, flags, resolved, json, write });
308
+ return;
309
+ }
310
+
311
+ write(buildHelpText());
312
+ }
package/src/generate.js CHANGED
@@ -9,6 +9,19 @@ export const DEFAULT_GENERATE_OPTIONS = {
9
9
  imageCount: 1,
10
10
  };
11
11
 
12
+ export const MAX_IMAGE_GENERATION_CONCURRENCY = 4;
13
+
14
+ export function normalizeImageCount(value, fallback = DEFAULT_GENERATE_OPTIONS.imageCount) {
15
+ const count = Number(value ?? fallback);
16
+ if (!Number.isFinite(count)) return fallback;
17
+ const normalized = Math.floor(count);
18
+ if (normalized < 1) throw new Error(`图片数量必须是 1 到 ${MAX_IMAGE_GENERATION_CONCURRENCY}`);
19
+ if (normalized > MAX_IMAGE_GENERATION_CONCURRENCY) {
20
+ throw new Error(`CLI 单次生图最多支持 ${MAX_IMAGE_GENERATION_CONCURRENCY} 张,请拆分多次调用。`);
21
+ }
22
+ return normalized;
23
+ }
24
+
12
25
  export function buildGeneratePayload({
13
26
  appId,
14
27
  groupId,