@yswgaicx/yswg-img-cli 0.1.0
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 +88 -0
- package/bin/yswg-img.js +126 -0
- package/package.json +25 -0
- package/src/api.js +101 -0
- package/src/args.js +50 -0
- package/src/config.js +40 -0
- package/src/generate.js +116 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# yswg-img-cli
|
|
2
|
+
|
|
3
|
+
`yswg-img` wraps the YSWG Monkey Genius image generation page as a CLI.
|
|
4
|
+
|
|
5
|
+
## Install Locally
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm link
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
You can also run it without linking:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
node bin/yswg-img.js --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Login
|
|
18
|
+
|
|
19
|
+
Send the enterprise WeChat verification code:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
yswg-img auth send-code --email zhangsan
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Login with the 6-digit code:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
yswg-img auth login --email zhangsan --code 123456
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The token is saved to `~/.yswg-img-cli/config.json`.
|
|
32
|
+
|
|
33
|
+
## Generate
|
|
34
|
+
|
|
35
|
+
Dry-run the backend payload:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
yswg-img generate \
|
|
39
|
+
--prompt "一张白底产品图,柔和自然光,高级电商摄影" \
|
|
40
|
+
--group-id 5 \
|
|
41
|
+
--model gemini-2.5-flash-image \
|
|
42
|
+
--ratio 1:1 \
|
|
43
|
+
--count 1 \
|
|
44
|
+
--dry-run \
|
|
45
|
+
--json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Submit and wait for output images:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
yswg-img generate \
|
|
52
|
+
--prompt "一张白底产品图,柔和自然光,高级电商摄影" \
|
|
53
|
+
--group-id 5 \
|
|
54
|
+
--model gemini-2.5-flash-image \
|
|
55
|
+
--ratio 1:1 \
|
|
56
|
+
--count 1 \
|
|
57
|
+
--out outputs \
|
|
58
|
+
--json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use reference images:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
yswg-img generate \
|
|
65
|
+
--prompt "参考产品主体,生成室内生活方式场景图" \
|
|
66
|
+
--group-id 5 \
|
|
67
|
+
--ref ./product.png,https://example.com/ref.jpg \
|
|
68
|
+
--out outputs \
|
|
69
|
+
--json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Submit only and return task IDs:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
yswg-img generate --prompt "..." --group-id 5 --no-wait --json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
Flags override environment variables, which override the saved config.
|
|
81
|
+
|
|
82
|
+
- `YSWG_TOKEN`
|
|
83
|
+
- `YSWG_REFRESH_TOKEN`
|
|
84
|
+
- `YSWG_APP_ID`
|
|
85
|
+
- `YSWG_BASE_URL`
|
|
86
|
+
- `YSWG_WS_PATH`
|
|
87
|
+
|
|
88
|
+
Default app ID: `2014153035982319618`.
|
package/bin/yswg-img.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
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 { buildGeneratePayload, downloadImages, extractImageUrls, uploadReferences, waitForTask } from "../src/generate.js";
|
|
6
|
+
|
|
7
|
+
function printHelp() {
|
|
8
|
+
console.log(`yswg-img
|
|
9
|
+
|
|
10
|
+
Commands:
|
|
11
|
+
auth send-code --email <name|email>
|
|
12
|
+
auth login --email <name|email> --code <6 digits>
|
|
13
|
+
models [--app-id <id>]
|
|
14
|
+
generate --prompt <text> --group-id <id> [--model <name>] [--ratio 1:1] [--count 1] [--ref a.png,b.jpg] [--out outputs]
|
|
15
|
+
|
|
16
|
+
Environment:
|
|
17
|
+
YSWG_TOKEN, YSWG_REFRESH_TOKEN, YSWG_APP_ID, YSWG_BASE_URL, YSWG_WS_PATH
|
|
18
|
+
|
|
19
|
+
Config:
|
|
20
|
+
${DEFAULT_CONFIG_PATH}
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function output(data, json = false) {
|
|
25
|
+
if (json) console.log(JSON.stringify(data, null, 2));
|
|
26
|
+
else if (typeof data === "string") console.log(data);
|
|
27
|
+
else console.log(JSON.stringify(data, null, 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function main() {
|
|
31
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
32
|
+
const { command, subcommand, flags } = parsed;
|
|
33
|
+
const json = Boolean(flags.json);
|
|
34
|
+
if (command === "help" || flags.help) return printHelp();
|
|
35
|
+
|
|
36
|
+
const config = await loadConfig();
|
|
37
|
+
const resolved = resolveConfig(config, flags);
|
|
38
|
+
const api = new YswgApi(resolved);
|
|
39
|
+
|
|
40
|
+
if (command === "auth" && subcommand === "send-code") {
|
|
41
|
+
const email = normalizeEmail(readFlag(flags, ["email", "e"]));
|
|
42
|
+
const check = await api.checkEmail(email);
|
|
43
|
+
if (check?.registered === false) throw new Error(check.message || "email is not registered");
|
|
44
|
+
await api.sendCode(email);
|
|
45
|
+
output({ ok: true, email, message: "验证码已发送,请在企业微信中查收" }, json);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (command === "auth" && subcommand === "login") {
|
|
50
|
+
const email = normalizeEmail(readFlag(flags, ["email", "e"]));
|
|
51
|
+
const code = String(readFlag(flags, ["code", "c"], "")).trim();
|
|
52
|
+
if (!code) throw new Error("missing --code");
|
|
53
|
+
const session = await api.login(email, code);
|
|
54
|
+
const next = {
|
|
55
|
+
...config,
|
|
56
|
+
baseUrl: resolved.baseUrl,
|
|
57
|
+
appId: resolved.appId,
|
|
58
|
+
wsPath: resolved.wsPath,
|
|
59
|
+
token: session.token,
|
|
60
|
+
refreshToken: session.refreshToken,
|
|
61
|
+
user: session,
|
|
62
|
+
email,
|
|
63
|
+
};
|
|
64
|
+
await saveConfig(next);
|
|
65
|
+
output({ ok: true, email, configPath: DEFAULT_CONFIG_PATH }, json);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (command === "models") {
|
|
70
|
+
const models = await api.models(resolved.appId);
|
|
71
|
+
output(models, json);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (command === "generate") {
|
|
76
|
+
if (!resolved.token) throw new Error("missing token; run auth login or set YSWG_TOKEN");
|
|
77
|
+
const prompt = String(readFlag(flags, ["prompt", "p"], "")).trim();
|
|
78
|
+
const refs = splitCsv(readFlag(flags, ["ref", "refs"], ""));
|
|
79
|
+
const refImages = await uploadReferences(api, refs);
|
|
80
|
+
const payload = buildGeneratePayload({
|
|
81
|
+
appId: resolved.appId,
|
|
82
|
+
groupId: String(readFlag(flags, ["groupId", "group"], "")),
|
|
83
|
+
prompt,
|
|
84
|
+
model: String(readFlag(flags, ["model", "m"], "gemini-2.5-flash-image")),
|
|
85
|
+
ratio: String(readFlag(flags, ["ratio", "r"], "1:1")),
|
|
86
|
+
imageCount: Number(readFlag(flags, ["count", "imageCount"], 1)),
|
|
87
|
+
refImages,
|
|
88
|
+
size: String(readFlag(flags, ["size"], "")),
|
|
89
|
+
night: Boolean(flags.night),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (flags.dryRun) {
|
|
93
|
+
output({ payload }, json);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const taskIds = await api.createTasks(payload);
|
|
98
|
+
const ids = Array.isArray(taskIds) ? taskIds : [taskIds?.taskId || taskIds?.id].filter(Boolean);
|
|
99
|
+
if (!ids.length) throw new Error("create task did not return task ids");
|
|
100
|
+
|
|
101
|
+
if (flags.noWait || flags.night) {
|
|
102
|
+
output({ ok: true, taskIds: ids, payload }, json);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const timeoutMs = Number(readFlag(flags, ["timeoutMs"], Number(readFlag(flags, ["timeout"], 3600)) * 1000));
|
|
107
|
+
const messages = await Promise.all(ids.map((taskId) => waitForTask({
|
|
108
|
+
baseUrl: resolved.baseUrl,
|
|
109
|
+
token: resolved.token,
|
|
110
|
+
wsPath: resolved.wsPath,
|
|
111
|
+
taskId,
|
|
112
|
+
timeoutMs,
|
|
113
|
+
})));
|
|
114
|
+
const imageUrls = messages.flatMap((message) => extractImageUrls(message.responseJson));
|
|
115
|
+
const files = flags.out === "none" ? [] : await downloadImages(imageUrls, String(readFlag(flags, ["out"], "outputs")));
|
|
116
|
+
output({ ok: true, taskIds: ids, imageUrls, files }, json);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
printHelp();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main().catch((error) => {
|
|
124
|
+
console.error(`yswg-img: ${error.message}`);
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yswgaicx/yswg-img-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI wrapper for YSWG Monkey Genius image generation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"yswg-img": "bin/yswg-img.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test",
|
|
16
|
+
"check": "node --check bin/yswg-img.js && node --check src/*.js"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22"
|
|
20
|
+
},
|
|
21
|
+
"license": "UNLICENSED",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export class YswgApi {
|
|
2
|
+
constructor({ baseUrl, token = "", refreshToken = "" }) {
|
|
3
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
4
|
+
this.token = token;
|
|
5
|
+
this.refreshToken = refreshToken;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async request(path, { method = "GET", data, headers = {}, raw = false, params } = {}) {
|
|
9
|
+
const url = new URL(`/prod-api${path}`, this.baseUrl);
|
|
10
|
+
if (params) {
|
|
11
|
+
for (const [key, value] of Object.entries(params)) {
|
|
12
|
+
if (value !== undefined && value !== null && value !== "") url.searchParams.set(key, value);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const requestHeaders = { ...headers };
|
|
17
|
+
let body;
|
|
18
|
+
if (data instanceof FormData) {
|
|
19
|
+
body = data;
|
|
20
|
+
} else if (data !== undefined) {
|
|
21
|
+
requestHeaders["Content-Type"] = "application/json;charset=utf-8";
|
|
22
|
+
body = JSON.stringify(data);
|
|
23
|
+
}
|
|
24
|
+
if (this.token) requestHeaders.Authorization = `Bearer ${this.token}`;
|
|
25
|
+
|
|
26
|
+
const response = await fetch(url, { method, headers: requestHeaders, body });
|
|
27
|
+
if (raw) return response;
|
|
28
|
+
|
|
29
|
+
const text = await response.text();
|
|
30
|
+
const json = text ? JSON.parse(text.replace(/(?<=[:,[]\s*)(\d{16,})(?=\s*[,}\]])/g, '"$1"')) : {};
|
|
31
|
+
if (!response.ok) throw new Error(json.message || `HTTP ${response.status}`);
|
|
32
|
+
const status = String(json.status ?? json.code ?? "");
|
|
33
|
+
if (status && status !== "200") {
|
|
34
|
+
const error = new Error(json.message || "request failed");
|
|
35
|
+
error.responseData = json;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
return Object.prototype.hasOwnProperty.call(json, "data") ? json.data : json;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
checkEmail(email) {
|
|
42
|
+
return this.request("/web/auth/email-checks", { params: { email } });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
sendCode(email) {
|
|
46
|
+
return this.request("/web/auth/email-codes", { method: "POST", data: { email } });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
login(email, code) {
|
|
50
|
+
return this.request("/web/auth/email-sessions", { method: "POST", data: { email, code } });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
refresh() {
|
|
54
|
+
return this.request("/web/auth/token-refresh", {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "Refresh-Token": `Bearer ${this.refreshToken}` },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
models(appId) {
|
|
61
|
+
return this.request(`/web/permissions/models/${appId}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
templates(appId) {
|
|
65
|
+
return this.request("/system/ai-prompt-templates/options", { params: { appId } });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
createTasks(payload) {
|
|
69
|
+
return this.request("/web/ai/invoke/tasks", { method: "POST", data: payload });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ossToken(scene = "ai_invoke") {
|
|
73
|
+
return this.request("/web/oss-tokens", { params: { scene } });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async uploadFile(file, filename, scene = "ai_invoke") {
|
|
77
|
+
const token = await this.ossToken(scene);
|
|
78
|
+
const info = token?.data || token;
|
|
79
|
+
const accessId = info.accessId || info.accessKeyId || info.accessid || info.AccessKeyId;
|
|
80
|
+
const policy = info.policy || info.Policy;
|
|
81
|
+
const signature = info.signature || info.Signature;
|
|
82
|
+
const host = info.host || info.Host;
|
|
83
|
+
const cdnHost = info.cdnHost || info.CdnHost || host;
|
|
84
|
+
const dir = info.dir || info.Dir || "";
|
|
85
|
+
if (!accessId || !policy || !signature || !host) throw new Error("invalid OSS token response");
|
|
86
|
+
|
|
87
|
+
const ext = filename.includes(".") ? filename.slice(filename.lastIndexOf(".")) : "";
|
|
88
|
+
const key = `${dir}${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
|
|
89
|
+
const form = new FormData();
|
|
90
|
+
form.append("key", key);
|
|
91
|
+
form.append("OSSAccessKeyId", accessId);
|
|
92
|
+
form.append("policy", policy);
|
|
93
|
+
form.append("signature", signature);
|
|
94
|
+
form.append("success_action_status", "200");
|
|
95
|
+
form.append("file", file, filename);
|
|
96
|
+
|
|
97
|
+
const response = await fetch(host, { method: "POST", body: form });
|
|
98
|
+
if (!response.ok) throw new Error(`OSS upload failed: HTTP ${response.status}`);
|
|
99
|
+
return `${cdnHost}/${key}`;
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/args.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function parseArgs(argv) {
|
|
2
|
+
const [command = "help", maybeSubcommand = "", ...tail] = argv;
|
|
3
|
+
const hasSubcommand = Boolean(maybeSubcommand) && !maybeSubcommand.startsWith("--");
|
|
4
|
+
const subcommand = hasSubcommand ? maybeSubcommand : "";
|
|
5
|
+
const rest = hasSubcommand ? tail : argv.slice(1);
|
|
6
|
+
const flags = {};
|
|
7
|
+
const positionals = [];
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
10
|
+
const item = rest[i];
|
|
11
|
+
if (!item.startsWith("--")) {
|
|
12
|
+
positionals.push(item);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const withoutPrefix = item.slice(2);
|
|
17
|
+
const [rawKey, inlineValue] = withoutPrefix.split(/=(.*)/s, 2);
|
|
18
|
+
const key = rawKey.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
19
|
+
if (inlineValue !== undefined) {
|
|
20
|
+
flags[key] = inlineValue;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const next = rest[i + 1];
|
|
25
|
+
if (next === undefined || next.startsWith("--")) {
|
|
26
|
+
flags[key] = true;
|
|
27
|
+
} else {
|
|
28
|
+
flags[key] = next;
|
|
29
|
+
i += 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { command, subcommand, flags, positionals };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function splitCsv(value) {
|
|
37
|
+
if (!value) return [];
|
|
38
|
+
if (Array.isArray(value)) return value;
|
|
39
|
+
return String(value)
|
|
40
|
+
.split(",")
|
|
41
|
+
.map((item) => item.trim())
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function readFlag(flags, names, fallback = undefined) {
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
if (flags[name] !== undefined) return flags[name];
|
|
48
|
+
}
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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_CONFIG_PATH = join(homedir(), ".yswg-img-cli", "config.json");
|
|
6
|
+
export const DEFAULT_BASE_URL = "https://www.yswg.love";
|
|
7
|
+
export const DEFAULT_APP_ID = "2014153035982319618";
|
|
8
|
+
export const DEFAULT_WS_PATH = "/websocket";
|
|
9
|
+
|
|
10
|
+
export async function loadConfig(path = DEFAULT_CONFIG_PATH) {
|
|
11
|
+
try {
|
|
12
|
+
const text = await readFile(path, "utf8");
|
|
13
|
+
return JSON.parse(text);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error.code === "ENOENT") return {};
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function saveConfig(config, path = DEFAULT_CONFIG_PATH) {
|
|
21
|
+
await mkdir(dirname(path), { recursive: true });
|
|
22
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveConfig(config, flags = {}) {
|
|
26
|
+
return {
|
|
27
|
+
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
|
+
token: flags.token || process.env.YSWG_TOKEN || config.token || "",
|
|
30
|
+
refreshToken: flags.refreshToken || process.env.YSWG_REFRESH_TOKEN || config.refreshToken || "",
|
|
31
|
+
wsPath: flags.wsPath || process.env.YSWG_WS_PATH || config.wsPath || DEFAULT_WS_PATH,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function normalizeEmail(input) {
|
|
36
|
+
const raw = String(input || "").trim();
|
|
37
|
+
if (!raw) throw new Error("missing email");
|
|
38
|
+
const prefix = raw.replace(/@yswg(?:\.com(?:\.cn)?)?$/i, "");
|
|
39
|
+
return `${prefix}@yswg.com.cn`;
|
|
40
|
+
}
|
package/src/generate.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
export function buildGeneratePayload({
|
|
5
|
+
appId,
|
|
6
|
+
groupId,
|
|
7
|
+
prompt,
|
|
8
|
+
model,
|
|
9
|
+
ratio = "1:1",
|
|
10
|
+
imageCount = 1,
|
|
11
|
+
refImages = [],
|
|
12
|
+
templates = [],
|
|
13
|
+
displayPrompt = "",
|
|
14
|
+
size = "",
|
|
15
|
+
night = false,
|
|
16
|
+
}) {
|
|
17
|
+
if (!appId) throw new Error("missing appId");
|
|
18
|
+
if (!groupId) throw new Error("missing groupId");
|
|
19
|
+
if (!model) throw new Error("missing model");
|
|
20
|
+
if (!prompt && templates.length === 0) throw new Error("missing prompt");
|
|
21
|
+
|
|
22
|
+
const params = { model };
|
|
23
|
+
if (prompt) params.prompt = prompt;
|
|
24
|
+
if (ratio) params.aspect_ratio = ratio;
|
|
25
|
+
if (refImages.length) params.image = refImages;
|
|
26
|
+
if (size) params.size = size;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
appId,
|
|
30
|
+
groupId,
|
|
31
|
+
params,
|
|
32
|
+
imageCount: Number(imageCount),
|
|
33
|
+
...(templates.length ? { templates } : {}),
|
|
34
|
+
...(displayPrompt ? { displayPrompt } : {}),
|
|
35
|
+
...(night ? { isNightGenerate: true } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function extractImageUrls(responseJson) {
|
|
40
|
+
const parsed = typeof responseJson === "string" ? JSON.parse(responseJson) : responseJson;
|
|
41
|
+
const data = parsed?.data ?? parsed?.result?.data ?? parsed?.images ?? parsed;
|
|
42
|
+
if (Array.isArray(data)) {
|
|
43
|
+
return data
|
|
44
|
+
.map((item) => (typeof item === "string" ? item : item?.url || item?.imageUrl))
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
if (typeof data === "string") return [data];
|
|
48
|
+
if (data?.url) return [data.url];
|
|
49
|
+
if (data?.imageUrl) return [data.imageUrl];
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function uploadReferences(api, refs) {
|
|
54
|
+
const urls = [];
|
|
55
|
+
for (const ref of refs) {
|
|
56
|
+
if (/^https?:\/\//i.test(ref)) {
|
|
57
|
+
urls.push(ref);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const bytes = await readFile(ref);
|
|
61
|
+
const blob = new Blob([bytes]);
|
|
62
|
+
urls.push(await api.uploadFile(blob, basename(ref)));
|
|
63
|
+
}
|
|
64
|
+
return urls;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function waitForTask({ baseUrl, token, wsPath, taskId, timeoutMs = 3600000 }) {
|
|
68
|
+
const protocol = baseUrl.startsWith("https:") ? "wss:" : "ws:";
|
|
69
|
+
const host = new URL(baseUrl).host;
|
|
70
|
+
const url = `${protocol}//${host}${wsPath}?token=${encodeURIComponent(token)}`;
|
|
71
|
+
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const ws = new WebSocket(url);
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
try {
|
|
76
|
+
ws.close();
|
|
77
|
+
} catch {}
|
|
78
|
+
reject(new Error(`task ${taskId} timed out`));
|
|
79
|
+
}, timeoutMs);
|
|
80
|
+
|
|
81
|
+
const cleanup = () => clearTimeout(timer);
|
|
82
|
+
ws.addEventListener("open", () => {
|
|
83
|
+
ws.send(JSON.stringify({ type: 3, taskId }));
|
|
84
|
+
ws.send(JSON.stringify({ type: 8888 }));
|
|
85
|
+
});
|
|
86
|
+
ws.addEventListener("message", (event) => {
|
|
87
|
+
const text = String(event.data).replace(/(?<=[:,[]\s*)(\d{16,})(?=\s*[,}\]])/g, '"$1"');
|
|
88
|
+
const msg = JSON.parse(text);
|
|
89
|
+
if (msg === 9999 || msg.type === 9999 || String(msg.taskId) !== String(taskId) || msg.status === 1) return;
|
|
90
|
+
cleanup();
|
|
91
|
+
ws.close();
|
|
92
|
+
if (msg.status === 2) resolve(msg);
|
|
93
|
+
else reject(new Error(msg.failReason || `task ${taskId} failed`));
|
|
94
|
+
});
|
|
95
|
+
ws.addEventListener("error", () => {
|
|
96
|
+
cleanup();
|
|
97
|
+
reject(new Error(`websocket failed for ${taskId}`));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function downloadImages(urls, outDir) {
|
|
103
|
+
await mkdir(outDir, { recursive: true });
|
|
104
|
+
const files = [];
|
|
105
|
+
for (let i = 0; i < urls.length; i += 1) {
|
|
106
|
+
const url = urls[i];
|
|
107
|
+
const response = await fetch(url);
|
|
108
|
+
if (!response.ok) throw new Error(`download failed: HTTP ${response.status} ${url}`);
|
|
109
|
+
const contentType = response.headers.get("content-type") || "";
|
|
110
|
+
const ext = contentType.includes("png") ? "png" : contentType.includes("webp") ? "webp" : "jpg";
|
|
111
|
+
const file = `${outDir.replace(/\/$/, "")}/image-${String(i + 1).padStart(2, "0")}.${ext}`;
|
|
112
|
+
await writeFile(file, Buffer.from(await response.arrayBuffer()));
|
|
113
|
+
files.push(file);
|
|
114
|
+
}
|
|
115
|
+
return files;
|
|
116
|
+
}
|