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.
Files changed (2) hide show
  1. package/bin/eprolo-cli.js +310 -0
  2. 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
+ }