@yswgaicx/yswg-img-cli 0.1.2 → 0.1.4

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