eprolo-cli 1.0.3
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/bin/eprolo-cli.js +310 -0
- package/package.json +24 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const http = require("http");
|
|
6
|
+
const https = require("https");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const readline = require("readline");
|
|
10
|
+
|
|
11
|
+
const BASE_URL = process.env.DXB_API_BASE_URL || "https://wixtest.eprolo.com/aw/auth";
|
|
12
|
+
const CONFIG_DIR = path.join(os.homedir(), ".dxb-toolkit");
|
|
13
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
14
|
+
|
|
15
|
+
function printHelp() {
|
|
16
|
+
console.log(`DXB CLI
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
dxb auth login --user-code <userCode> [--default-store-name <storeName>] [--account <emailOrName>]
|
|
20
|
+
dxb auth status
|
|
21
|
+
dxb auth logout
|
|
22
|
+
dxb batch submit --store-name <storeName> --action ai_release_products --source-url-list <urls> [--pt-name <platform>] [--data-json <json>] [--task-key <key>]
|
|
23
|
+
dxb status ai --plan-id <planId> --store-name <storeName> [--user-code <userCode>]
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
--base-url <url> Override API base URL for this command.
|
|
27
|
+
--user-code <value> 店小宝用户密文 from https://erp.dianxiaobao.net/basic/account/setting.
|
|
28
|
+
--default-store-name <value>
|
|
29
|
+
Optional default authorized shop name for multi-store accounts.
|
|
30
|
+
--account <value> Optional plain account label shown in the plugin authorization card.
|
|
31
|
+
--store-name <value> Target authorized shop name for this task.
|
|
32
|
+
--pt-name <value> Optional target platform for this task, for example icbu.
|
|
33
|
+
--json Print JSON only.
|
|
34
|
+
|
|
35
|
+
Authorization onboarding:
|
|
36
|
+
1. Register or log in to 店小宝: https://erp.dianxiaobao.net/
|
|
37
|
+
2. Copy 用户密文 as userCode: https://erp.dianxiaobao.net/basic/account/setting
|
|
38
|
+
3. Save the profile:
|
|
39
|
+
dxb auth login --user-code <userCode> --default-store-name <storeName>
|
|
40
|
+
|
|
41
|
+
Task targeting:
|
|
42
|
+
店小宝 supports multiple shops. The auth profile may include a default store,
|
|
43
|
+
but an explicit --store-name always wins. Ask the user to specify the
|
|
44
|
+
authorized shop per task when the target is not clear, for example:
|
|
45
|
+
"在 33333 店铺下,用这些货源链接 AI 发品..."
|
|
46
|
+
If the user also provides a platform such as icbu, pass it with --pt-name.
|
|
47
|
+
|
|
48
|
+
Status checks:
|
|
49
|
+
Status APIs also require userCode. The CLI uses the saved auth profile, or
|
|
50
|
+
accepts --user-code for one-off checks. Always pass the same --store-name
|
|
51
|
+
used for submission.
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const args = { _: [] };
|
|
57
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
58
|
+
const token = argv[i];
|
|
59
|
+
if (!token.startsWith("--")) {
|
|
60
|
+
args._.push(token);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const key = token.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
64
|
+
const next = argv[i + 1];
|
|
65
|
+
if (!next || next.startsWith("--")) {
|
|
66
|
+
args[key] = true;
|
|
67
|
+
} else {
|
|
68
|
+
args[key] = next;
|
|
69
|
+
i += 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return args;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readConfig() {
|
|
76
|
+
if (!fs.existsSync(CONFIG_FILE)) return {};
|
|
77
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeConfig(config) {
|
|
81
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
82
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function promptInput(question) {
|
|
86
|
+
const rl = readline.createInterface({
|
|
87
|
+
input: process.stdin,
|
|
88
|
+
output: process.stdout
|
|
89
|
+
});
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
rl.question(question, (answer) => {
|
|
92
|
+
rl.close();
|
|
93
|
+
resolve(answer.trim());
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function requireAuth(args) {
|
|
99
|
+
const config = readConfig();
|
|
100
|
+
const userCode = args.userCode || args.user_code || process.env.DXB_USER_CODE || config.userCode;
|
|
101
|
+
if (!userCode) {
|
|
102
|
+
throw new Error("Missing authorization profile. Log in to https://erp.dianxiaobao.net/, copy 用户密文 from https://erp.dianxiaobao.net/basic/account/setting as userCode, then run: dxb auth login --user-code <userCode>");
|
|
103
|
+
}
|
|
104
|
+
return { userCode };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function requireTaskTarget(args) {
|
|
108
|
+
const config = readConfig();
|
|
109
|
+
const ptName = args.ptName;
|
|
110
|
+
const storeName = args.storeName || args.defaultStoreName || process.env.DXB_STORE_NAME || process.env.DXB_DEFAULT_STORE_NAME || config.defaultStoreName;
|
|
111
|
+
if (!storeName) {
|
|
112
|
+
throw new Error("Missing task target. 店小宝 supports multiple shops. Provide --store-name for this task, or save a default store with: dxb auth login --user-code <userCode> --default-store-name <storeName>");
|
|
113
|
+
}
|
|
114
|
+
return { ptName, storeName };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function splitIds(value) {
|
|
118
|
+
if (!value) return [];
|
|
119
|
+
return String(value).split(",").map((item) => item.trim()).filter(Boolean);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function taskKey(action) {
|
|
123
|
+
return `${action}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isTemplatePlaceholder(value) {
|
|
127
|
+
return typeof value === "string" && /^\$\{[^}]+\}$/.test(value.trim());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeCredentialValue(value) {
|
|
131
|
+
return isTemplatePlaceholder(value) ? "" : value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function postJson(baseUrl, pathName, body) {
|
|
135
|
+
const url = `${baseUrl}${pathName}`;
|
|
136
|
+
if (typeof fetch === "function") {
|
|
137
|
+
const res = await fetch(url, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: { "content-type": "application/json" },
|
|
140
|
+
body: JSON.stringify(body)
|
|
141
|
+
});
|
|
142
|
+
const text = await res.text();
|
|
143
|
+
const data = parseJsonResponse(text);
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
const err = new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
146
|
+
err.response = data;
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
return data;
|
|
150
|
+
}
|
|
151
|
+
return postJsonWithNodeHttp(url, body);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseJsonResponse(text) {
|
|
155
|
+
try {
|
|
156
|
+
return text ? JSON.parse(text) : {};
|
|
157
|
+
} catch (_) {
|
|
158
|
+
return { raw: text };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function postJsonWithNodeHttp(url, body) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const target = new URL(url);
|
|
165
|
+
const payload = JSON.stringify(body);
|
|
166
|
+
const transport = target.protocol === "https:" ? https : http;
|
|
167
|
+
const req = transport.request({
|
|
168
|
+
protocol: target.protocol,
|
|
169
|
+
hostname: target.hostname,
|
|
170
|
+
port: target.port,
|
|
171
|
+
path: `${target.pathname}${target.search}`,
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
"content-type": "application/json",
|
|
175
|
+
"content-length": Buffer.byteLength(payload)
|
|
176
|
+
}
|
|
177
|
+
}, (res) => {
|
|
178
|
+
const chunks = [];
|
|
179
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
180
|
+
res.on("end", () => {
|
|
181
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
182
|
+
const data = parseJsonResponse(text);
|
|
183
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
184
|
+
const err = new Error(`HTTP ${res.statusCode} ${res.statusMessage || ""}`.trim());
|
|
185
|
+
err.response = data;
|
|
186
|
+
reject(err);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
resolve(data);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
req.on("error", reject);
|
|
193
|
+
req.write(payload);
|
|
194
|
+
req.end();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function printResult(data, jsonOnly) {
|
|
199
|
+
if (jsonOnly) {
|
|
200
|
+
console.log(JSON.stringify(data, null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
console.log(JSON.stringify(data, null, 2));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function main() {
|
|
207
|
+
const args = parseArgs(process.argv.slice(2));
|
|
208
|
+
const [group, command] = args._;
|
|
209
|
+
const baseUrl = args.baseUrl || BASE_URL;
|
|
210
|
+
|
|
211
|
+
if (!group || args.help || args.h) {
|
|
212
|
+
printHelp();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (group === "auth" && command === "login") {
|
|
217
|
+
let userCode = normalizeCredentialValue(args.userCode || args.user_code || process.env.DXB_USER_CODE || process.env.userCode || process.env.USER_CODE);
|
|
218
|
+
let defaultStoreName = normalizeCredentialValue(args.defaultStoreName || args.default_store_name || args.storeName || process.env.DXB_DEFAULT_STORE_NAME || process.env.DXB_STORE_NAME || process.env.defaultStoreName || process.env.storeName || process.env.STORE_NAME);
|
|
219
|
+
let account = normalizeCredentialValue(args.account || process.env.DXB_ACCOUNT || process.env.account || process.env.ACCOUNT || defaultStoreName || null);
|
|
220
|
+
if (!userCode && process.stdin.isTTY) {
|
|
221
|
+
console.log("店小宝授权需要用户密文。");
|
|
222
|
+
console.log("1. 登录 https://erp.dianxiaobao.net/");
|
|
223
|
+
console.log("2. 打开 https://erp.dianxiaobao.net/basic/account/setting");
|
|
224
|
+
console.log("3. 复制用户密文并粘贴到下方。");
|
|
225
|
+
userCode = await promptInput("用户密文 userCode: ");
|
|
226
|
+
}
|
|
227
|
+
if (userCode && !defaultStoreName && process.stdin.isTTY) {
|
|
228
|
+
defaultStoreName = await promptInput("默认店铺 defaultStoreName: ");
|
|
229
|
+
}
|
|
230
|
+
if (!account && defaultStoreName) account = defaultStoreName;
|
|
231
|
+
if (!userCode) {
|
|
232
|
+
throw new Error("auth login requires --user-code, DXB_USER_CODE, connector field injection, or an interactive terminal to enter userCode");
|
|
233
|
+
}
|
|
234
|
+
const config = {
|
|
235
|
+
userCode
|
|
236
|
+
};
|
|
237
|
+
if (defaultStoreName) config.defaultStoreName = defaultStoreName;
|
|
238
|
+
if (account) config.account = account;
|
|
239
|
+
writeConfig(config);
|
|
240
|
+
printResult({
|
|
241
|
+
success: true,
|
|
242
|
+
connected: true,
|
|
243
|
+
account: config.account || null,
|
|
244
|
+
defaultStoreName: config.defaultStoreName || null
|
|
245
|
+
}, args.json);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (group === "auth" && command === "status") {
|
|
250
|
+
const config = readConfig();
|
|
251
|
+
printResult({
|
|
252
|
+
connected: Boolean(config.userCode),
|
|
253
|
+
account: config.account || null,
|
|
254
|
+
defaultStoreName: config.defaultStoreName || null
|
|
255
|
+
}, args.json);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (group === "auth" && command === "logout") {
|
|
260
|
+
if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
|
|
261
|
+
printResult({ success: true }, args.json);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (group === "batch" && command === "submit") {
|
|
266
|
+
if (!args.action) throw new Error("batch submit requires --action");
|
|
267
|
+
if (args.action !== "ai_release_products") {
|
|
268
|
+
throw new Error("Only ai_release_products is supported in this simplified version.");
|
|
269
|
+
}
|
|
270
|
+
const auth = requireAuth(args);
|
|
271
|
+
const target = requireTaskTarget(args);
|
|
272
|
+
const productIds = splitIds(args.productIds);
|
|
273
|
+
const body = {
|
|
274
|
+
action: args.action,
|
|
275
|
+
taskKey: args.taskKey || taskKey(args.action),
|
|
276
|
+
userCode: auth.userCode,
|
|
277
|
+
storeName: target.storeName,
|
|
278
|
+
productIds
|
|
279
|
+
};
|
|
280
|
+
if (target.ptName) body.ptName = target.ptName;
|
|
281
|
+
if (args.payloadJson) body.payload = JSON.parse(args.payloadJson);
|
|
282
|
+
if (args.dataJson) body.data = JSON.parse(args.dataJson);
|
|
283
|
+
if (args.sourceUrlList) body.sourceUrlList = splitIds(args.sourceUrlList);
|
|
284
|
+
printResult(await postJson(baseUrl, "/submit", body), args.json);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (group === "status" && command === "ai") {
|
|
289
|
+
if (!args.planId) throw new Error("status ai requires --plan-id");
|
|
290
|
+
const auth = requireAuth(args);
|
|
291
|
+
const target = requireTaskTarget(args);
|
|
292
|
+
printResult(await postJson(baseUrl, "/aiReleaseStatus", {
|
|
293
|
+
planId: args.planId,
|
|
294
|
+
userCode: auth.userCode,
|
|
295
|
+
storeName: target.storeName
|
|
296
|
+
}), args.json);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
throw new Error(`Unknown command: ${process.argv.slice(2).join(" ")}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
main().catch((err) => {
|
|
304
|
+
console.error(JSON.stringify({
|
|
305
|
+
success: false,
|
|
306
|
+
errorMessage: err.message,
|
|
307
|
+
response: err.response || null
|
|
308
|
+
}, null, 2));
|
|
309
|
+
process.exit(1);
|
|
310
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eprolo-cli",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "CLI for Dianxiaobao ICBU AI publishing, authorization profile management, task submission, and task status checks.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"eprolo": "bin/eprolo-cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/eprolo-cli.js"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"eprolo",
|
|
13
|
+
"dxb",
|
|
14
|
+
"icbu",
|
|
15
|
+
"alibaba",
|
|
16
|
+
"e-commerce",
|
|
17
|
+
"cli"
|
|
18
|
+
],
|
|
19
|
+
"author": "店小宝",
|
|
20
|
+
"license": "UNLICENSED",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
}
|
|
24
|
+
}
|