eprolo-cli 1.0.27 → 1.0.28

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 +244 -197
  2. package/package.json +1 -1
package/bin/eprolo-cli.js CHANGED
@@ -2,56 +2,34 @@
2
2
  "use strict";
3
3
 
4
4
  const fs = require("fs");
5
- const http = require("http");
6
- const https = require("https");
7
5
  const os = require("os");
8
6
  const path = require("path");
9
- const readline = require("readline");
7
+ const https = require("https");
10
8
 
11
- const BASE_URL = process.env.DXB_API_BASE_URL || "https://wixtest.eprolo.com/aw/auth";
9
+ const VERIFY_URL = "https://wixtest.eprolo.com//v1/auth/verify-code";
12
10
  const CONFIG_DIR = path.join(os.homedir(), ".dxb-toolkit");
13
11
  const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
14
12
 
15
- function printHelp() {
16
- console.log(`DXB CLI
13
+ // ---------------------------------------------------------------------------
14
+ // 凭据文件读写(~/.dxb-toolkit/config.json)
15
+ // ---------------------------------------------------------------------------
17
16
 
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
- `);
17
+ function readConfig() {
18
+ if (!fs.existsSync(CONFIG_FILE)) return {};
19
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
53
20
  }
54
21
 
22
+ function writeConfig(config) {
23
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
24
+ const tmp = CONFIG_FILE + ".tmp";
25
+ fs.writeFileSync(tmp, JSON.stringify(config, null, 2), "utf8");
26
+ fs.renameSync(tmp, CONFIG_FILE);
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // 参数解析
31
+ // ---------------------------------------------------------------------------
32
+
55
33
  function parseArgs(argv) {
56
34
  const args = { _: [] };
57
35
  for (let i = 0; i < argv.length; i += 1) {
@@ -72,210 +50,279 @@ function parseArgs(argv) {
72
50
  return args;
73
51
  }
74
52
 
75
- function readConfig() {
76
- if (!fs.existsSync(CONFIG_FILE)) return {};
77
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
53
+ function readFlag(argv, flag) {
54
+ const idx = argv.indexOf(flag);
55
+ if (idx === -1 || idx + 1 >= argv.length) return undefined;
56
+ return argv[idx + 1];
78
57
  }
79
58
 
80
- function writeConfig(config) {
81
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
82
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
83
- }
59
+ // ---------------------------------------------------------------------------
60
+ // HTTP POST 请求
61
+ // ---------------------------------------------------------------------------
84
62
 
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
- });
63
+ function postJson(url, body) {
64
+ return new Promise((resolve, reject) => {
65
+ const target = new URL(url);
66
+ const payload = JSON.stringify(body);
67
+ const req = https.request(
68
+ {
69
+ hostname: target.hostname,
70
+ port: target.port || 443,
71
+ path: target.pathname + target.search,
72
+ method: "POST",
73
+ headers: {
74
+ "Content-Type": "application/json",
75
+ "Content-Length": Buffer.byteLength(payload)
76
+ }
77
+ },
78
+ (res) => {
79
+ const chunks = [];
80
+ res.on("data", (chunk) => chunks.push(chunk));
81
+ res.on("end", () => {
82
+ const text = Buffer.concat(chunks).toString("utf8");
83
+ let data;
84
+ try {
85
+ data = text ? JSON.parse(text) : {};
86
+ } catch (_) {
87
+ data = { raw: text };
88
+ }
89
+ if (res.statusCode < 200 || res.statusCode >= 300) {
90
+ const err = new Error(`HTTP ${res.statusCode}`);
91
+ err.response = data;
92
+ reject(err);
93
+ return;
94
+ }
95
+ resolve(data);
96
+ });
97
+ }
98
+ );
99
+ req.on("error", reject);
100
+ req.write(payload);
101
+ req.end();
95
102
  });
96
103
  }
97
104
 
98
- function requireAuth(args) {
99
- const config = readConfig();
100
- const userCode = args.userCode || args.user_code || process.env.DXB_USER_CODE || config.userCode || "mock-user-code";
101
- return { userCode };
102
- }
105
+ // ---------------------------------------------------------------------------
106
+ // 登录命令
107
+ // 输入:env 通道 DXB_USER_CODE + arg 通道 --default-shop
108
+ // 流程:调用 verify-code 接口校验密文 → 写凭据文件 → exit 0
109
+ // ---------------------------------------------------------------------------
110
+
111
+ async function login(argv) {
112
+ // env 通道:框架通过 fieldInjection 注入 DXB_USER_CODE
113
+ const userCode =
114
+ process.env.DXB_USER_CODE ||
115
+ readFlag(argv, "--user-code") ||
116
+ readFlag(argv, "--userCode");
117
+
118
+ if (!userCode) {
119
+ console.error(JSON.stringify({
120
+ success: false,
121
+ errorMessage: "缺少用户密文:请在授权弹窗中填写,或设置环境变量 DXB_USER_CODE"
122
+ }, null, 2));
123
+ process.exit(2);
124
+ }
103
125
 
104
- function requireTaskTarget(args) {
105
- const config = readConfig();
106
- const ptName = args.ptName;
107
- const storeName = args.storeName || args.defaultStoreName || process.env.DXB_STORE_NAME || process.env.DXB_DEFAULT_STORE_NAME || config.defaultStoreName || "mock-store";
108
- return { ptName, storeName };
109
- }
126
+ // arg 通道:框架通过 fieldInjection 追加 --default-shop
127
+ const defaultShop =
128
+ readFlag(argv, "--default-shop") ||
129
+ readFlag(argv, "--default-store-name") ||
130
+ readFlag(argv, "--store-name") ||
131
+ process.env.DXB_STORE_NAME ||
132
+ null;
133
+
134
+ // 账号标识
135
+ const accountAlias =
136
+ readFlag(argv, "--account") ||
137
+ process.env.DXB_ACCOUNT ||
138
+ null;
139
+
140
+ // 调用 verify-code 接口校验密文
141
+ let verifyResult;
142
+ try {
143
+ verifyResult = await postJson(VERIFY_URL, { userCode });
144
+ } catch (err) {
145
+ console.error(JSON.stringify({
146
+ success: false,
147
+ errorMessage: "密文无效或已吊销,请到账号设置页重新复制",
148
+ detail: err.message,
149
+ response: err.response || null
150
+ }, null, 2));
151
+ process.exit(1);
152
+ }
110
153
 
111
- function splitIds(value) {
112
- if (!value) return [];
113
- return String(value).split(",").map((item) => item.trim()).filter(Boolean);
154
+ // 校验成功,写凭据文件
155
+ const account = accountAlias || verifyResult.account || defaultShop || "unknown";
156
+ const config = {
157
+ account, // 顶层明文,框架读它做卡片标签
158
+ userCode,
159
+ accessToken: verifyResult.accessToken || null,
160
+ refreshToken: verifyResult.refreshToken || null,
161
+ defaultShop: defaultShop || verifyResult.defaultShop || null,
162
+ version: 1
163
+ };
164
+
165
+ writeConfig(config);
166
+
167
+ // 不打印敏感值
168
+ console.log(JSON.stringify({
169
+ success: true,
170
+ connected: true,
171
+ account: config.account,
172
+ defaultShop: config.defaultShop
173
+ }, null, 2));
174
+ process.exit(0);
114
175
  }
115
176
 
116
- function taskKey(action) {
117
- return `${action}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
118
- }
177
+ // ---------------------------------------------------------------------------
178
+ // 授权状态
179
+ // ---------------------------------------------------------------------------
119
180
 
120
- function isTemplatePlaceholder(value) {
121
- return typeof value === "string" && /^\$\{[^}]+\}$/.test(value.trim());
181
+ function authStatus() {
182
+ const config = readConfig();
183
+ const connected = fs.existsSync(CONFIG_FILE) && Boolean(config.userCode);
184
+ console.log(JSON.stringify({
185
+ connected,
186
+ account: config.account || null,
187
+ defaultShop: config.defaultShop || null
188
+ }, null, 2));
189
+ process.exit(0);
122
190
  }
123
191
 
124
- function normalizeCredentialValue(value) {
125
- return isTemplatePlaceholder(value) ? "" : value;
126
- }
192
+ // ---------------------------------------------------------------------------
193
+ // 授权验证
194
+ // 读取凭据文件 → 调用 verify-code 接口验证密文是否仍然有效
195
+ // ---------------------------------------------------------------------------
127
196
 
128
- async function postJson(baseUrl, pathName, body) {
129
- const url = `${baseUrl}${pathName}`;
130
- if (url.includes("/submit")) {
131
- return { success: true, message: "Task submitted successfully", taskKey: body.taskKey };
132
- }
133
- if (url.includes("/aiReleaseStatus")) {
134
- return { success: true, planId: body.planId, status: "processing" };
197
+ async function authVerify(argv) {
198
+ const config = readConfig();
199
+
200
+ if (!config.userCode) {
201
+ console.error(JSON.stringify({
202
+ success: false,
203
+ connected: false,
204
+ errorMessage: "未连接或凭据不存在,请先执行 auth login"
205
+ }, null, 2));
206
+ process.exit(1);
135
207
  }
136
- return { success: true, raw: body };
137
- }
138
208
 
139
- function parseJsonResponse(text) {
209
+ // 支持传入额外 userCode 覆盖
210
+ const userCode =
211
+ readFlag(argv, "--user-code") ||
212
+ process.env.DXB_USER_CODE ||
213
+ config.userCode;
214
+
140
215
  try {
141
- return text ? JSON.parse(text) : {};
142
- } catch (_) {
143
- return { raw: text };
216
+ const result = await postJson(VERIFY_URL, { userCode });
217
+ console.log(JSON.stringify({
218
+ success: true,
219
+ connected: true,
220
+ valid: true,
221
+ account: config.account,
222
+ accountFromServer: result.account || null,
223
+ defaultShop: config.defaultShop
224
+ }, null, 2));
225
+ process.exit(0);
226
+ } catch (err) {
227
+ console.error(JSON.stringify({
228
+ success: false,
229
+ connected: false,
230
+ valid: false,
231
+ errorMessage: "凭据验证失败:密文无效或已过期,请重新登录",
232
+ detail: err.message,
233
+ response: err.response || null
234
+ }, null, 2));
235
+ process.exit(1);
144
236
  }
145
237
  }
146
238
 
147
- function postJsonWithNodeHttp(url, body) {
148
- return new Promise((resolve, reject) => {
149
- const target = new URL(url);
150
- const payload = JSON.stringify(body);
151
- const transport = target.protocol === "https:" ? https : http;
152
- const req = transport.request({
153
- protocol: target.protocol,
154
- hostname: target.hostname,
155
- port: target.port,
156
- path: `${target.pathname}${target.search}`,
157
- method: "POST",
158
- headers: {
159
- "content-type": "application/json",
160
- "content-length": Buffer.byteLength(payload)
161
- }
162
- }, (res) => {
163
- const chunks = [];
164
- res.on("data", (chunk) => chunks.push(chunk));
165
- res.on("end", () => {
166
- const text = Buffer.concat(chunks).toString("utf8");
167
- const data = parseJsonResponse(text);
168
- if (res.statusCode < 200 || res.statusCode >= 300) {
169
- const err = new Error(`HTTP ${res.statusCode} ${res.statusMessage || ""}`.trim());
170
- err.response = data;
171
- reject(err);
172
- return;
173
- }
174
- resolve(data);
175
- });
176
- });
177
- req.on("error", reject);
178
- req.write(payload);
179
- req.end();
180
- });
239
+ // ---------------------------------------------------------------------------
240
+ // 退出授权
241
+ // ---------------------------------------------------------------------------
242
+
243
+ function authLogout() {
244
+ if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
245
+ console.log(JSON.stringify({ success: true }, null, 2));
246
+ process.exit(0);
181
247
  }
182
248
 
183
- function printResult(data, jsonOnly) {
184
- if (jsonOnly) {
185
- console.log(JSON.stringify(data, null, 2));
186
- return;
187
- }
188
- console.log(JSON.stringify(data, null, 2));
249
+ // ---------------------------------------------------------------------------
250
+ // 帮助
251
+ // ---------------------------------------------------------------------------
252
+
253
+ function printHelp() {
254
+ console.log(`DXB CLI
255
+
256
+ Usage:
257
+ eprolo-cli auth login [--default-shop <shop>] [--account <alias>]
258
+ 登录:调用 verify-code 校验密文,写凭据文件
259
+ eprolo-cli auth verify [--user-code <code>]
260
+ 验证:调用 verify-code 检查密文是否仍然有效
261
+ eprolo-cli auth status
262
+ 查看本地授权状态
263
+ eprolo-cli auth logout
264
+ 退出授权,删除凭据文件
265
+
266
+ Authorization:
267
+ 框架通过 fieldInjection 把表单值递交给 CLI:
268
+ userCode → env 通道 → process.env.DXB_USER_CODE
269
+ defaultShop → arg 通道 → --default-shop
270
+
271
+ 连接成功的唯一判定:凭据文件 ~/.dxb-toolkit/config.json 出现
272
+ 凭据文件顶层明文 account 字段用于授权卡片标签
273
+
274
+ Verify API:
275
+ POST https://wixtest.eprolo.com//v1/auth/verify-code
276
+ Body: { "userCode": "<用户密文>" }
277
+ 200 ← { "account": "...", "accessToken": "...", ... }
278
+ 401 ← { "error": "invalid_or_revoked_code" }
279
+ `);
189
280
  }
190
281
 
282
+ // ---------------------------------------------------------------------------
283
+ // 入口
284
+ // ---------------------------------------------------------------------------
285
+
191
286
  async function main() {
192
- const args = parseArgs(process.argv.slice(2));
193
- const [group, command] = args._;
194
- const baseUrl = args.baseUrl || BASE_URL;
287
+ const argv = process.argv.slice(2);
288
+ const [group, command] = argv;
195
289
 
196
- if (!group || args.help || args.h) {
290
+ if (!group || group === "--help" || group === "-h") {
197
291
  printHelp();
198
292
  return;
199
293
  }
200
294
 
201
295
  if (group === "auth" && command === "login") {
202
- const config = {
203
- userCode: args.userCode || args.user_code || process.env.DXB_USER_CODE || process.env.userCode || process.env.USER_CODE || "mock-user-code"
204
- };
205
- const defaultStoreName = 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;
206
- const account = args.account || process.env.DXB_ACCOUNT || process.env.account || process.env.ACCOUNT || defaultStoreName || "mock-account";
207
- if (defaultStoreName) config.defaultStoreName = defaultStoreName;
208
- config.account = account;
209
- writeConfig(config);
210
- printResult({
211
- success: true,
212
- connected: true,
213
- account: config.account || null,
214
- defaultStoreName: config.defaultStoreName || null
215
- }, args.json);
216
- return;
217
- }
218
-
219
- if (group === "auth" && command === "status") {
220
- const config = readConfig();
221
- printResult({
222
- connected: true,
223
- account: config.account || "mock-account",
224
- defaultStoreName: config.defaultStoreName || "mock-store"
225
- }, args.json);
296
+ await login(argv.slice(2));
226
297
  return;
227
298
  }
228
299
 
229
- if (group === "auth" && command === "logout") {
230
- if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
231
- printResult({ success: true }, args.json);
300
+ if (group === "auth" && command === "verify") {
301
+ await authVerify(argv.slice(2));
232
302
  return;
233
303
  }
234
304
 
235
- if (group === "batch" && command === "submit") {
236
- // if (!args.action) throw new Error("batch submit requires --action");
237
- // if (args.action !== "ai_release_products") {
238
- // throw new Error("Only ai_release_products is supported in this simplified version.");
239
- // }
240
- const auth = requireAuth(args);
241
- const target = requireTaskTarget(args);
242
- const productIds = splitIds(args.productIds);
243
- const body = {
244
- action: args.action,
245
- taskKey: args.taskKey || taskKey(args.action),
246
- userCode: auth.userCode,
247
- storeName: target.storeName,
248
- productIds
249
- };
250
- if (target.ptName) body.ptName = target.ptName;
251
- if (args.payloadJson) body.payload = JSON.parse(args.payloadJson);
252
- if (args.dataJson) body.data = JSON.parse(args.dataJson);
253
- if (args.sourceUrlList) body.sourceUrlList = splitIds(args.sourceUrlList);
254
- printResult(await postJson(baseUrl, "/submit", body), args.json);
305
+ if (group === "auth" && command === "status") {
306
+ authStatus();
255
307
  return;
256
308
  }
257
309
 
258
- if (group === "status" && command === "ai") {
259
- if (!args.planId) throw new Error("status ai requires --plan-id");
260
- const auth = requireAuth(args);
261
- const target = requireTaskTarget(args);
262
- printResult(await postJson(baseUrl, "/aiReleaseStatus", {
263
- planId: args.planId,
264
- userCode: auth.userCode,
265
- storeName: target.storeName
266
- }), args.json);
310
+ if (group === "auth" && command === "logout") {
311
+ authLogout();
267
312
  return;
268
313
  }
269
314
 
270
- throw new Error(`Unknown command: ${process.argv.slice(2).join(" ")}`);
315
+ console.error(JSON.stringify({
316
+ success: false,
317
+ errorMessage: `Unknown command: ${argv.join(" ")}`
318
+ }, null, 2));
319
+ process.exit(1);
271
320
  }
272
321
 
273
322
  main().catch((err) => {
274
323
  console.error(JSON.stringify({
275
324
  success: false,
276
- errorMessage: err.message,
277
- response: err.response || null
325
+ errorMessage: err.message
278
326
  }, null, 2));
279
327
  process.exit(1);
280
328
  });
281
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eprolo-cli",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
4
4
  "description": "CLI for Dianxiaobao ICBU AI publishing, authorization profile management, task submission, and task status checks.",
5
5
  "bin": {
6
6
  "eprolo": "bin/eprolo-cli.js"