@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 +67 -2
- package/bin/yswg-img.js +2 -253
- package/package.json +1 -1
- package/src/api.js +4 -0
- package/src/cli.js +312 -0
- package/src/generate.js +13 -0
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
|
-
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
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,
|