eprolo-cli 1.0.31 → 1.0.32

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 +250 -176
  2. package/package.json +1 -1
package/bin/eprolo-cli.js CHANGED
@@ -4,14 +4,14 @@
4
4
  const fs = require("fs");
5
5
  const os = require("os");
6
6
  const path = require("path");
7
- const https = require("https");
8
7
 
9
- const VERIFY_URL = "https://wixtest.eprolo.com/v1/auth/verify-code";
10
8
  const CONFIG_DIR = path.join(os.homedir(), ".dxb-toolkit");
11
9
  const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
12
10
 
13
11
  // ---------------------------------------------------------------------------
14
- // 凭据文件读写(~/.dxb-toolkit/config.json)
12
+ // D3 · 凭据文件读写(~/.dxb-toolkit/config.json)
13
+ // 框架判定连接成功的唯一依据:凭据文件出现
14
+ // 框架读顶层明文 account 字段做授权卡片标签
15
15
  // ---------------------------------------------------------------------------
16
16
 
17
17
  function readConfig() {
@@ -21,6 +21,7 @@ function readConfig() {
21
21
 
22
22
  function writeConfig(config) {
23
23
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
24
+ // 原子写:临时文件 + rename,避免 MCP 进程读到半截 JSON
24
25
  const tmp = CONFIG_FILE + ".tmp";
25
26
  fs.writeFileSync(tmp, JSON.stringify(config, null, 2), "utf8");
26
27
  fs.renameSync(tmp, CONFIG_FILE);
@@ -56,72 +57,29 @@ function readFlag(argv, flag) {
56
57
  return argv[idx + 1];
57
58
  }
58
59
 
59
- // ---------------------------------------------------------------------------
60
- // HTTP POST 请求
61
- // ---------------------------------------------------------------------------
60
+ function splitIds(value) {
61
+ if (!value) return [];
62
+ return String(value).split(",").map((item) => item.trim()).filter(Boolean);
63
+ }
62
64
 
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();
102
- });
65
+ function taskKey(action) {
66
+ return `${action}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
103
67
  }
104
68
 
105
69
  // ---------------------------------------------------------------------------
106
- // 登录命令
107
- // 输入:env 通道 DXB_USER_CODE + arg 通道 --default-shop
108
- // 流程:调用 verify-code 接口校验密文 → 写凭据文件 → exit 0
70
+ // D2 · login 命令
71
+ // 输入契约(框架 CLI):
72
+ // userCode ← env 通道 process.env.DXB_USER_CODE
73
+ // defaultShop ← arg 通道 --default-shop(或 --default-store-name 兼容旧版)
74
+ // 行为契约:
75
+ // 成功 → 写凭据文件 + exit 0
76
+ // 失败 → 非零退出 + stderr 错误信息
77
+ // ❌ 禁止交互式 prompt(CLI 跑在非交互终端)
109
78
  // ---------------------------------------------------------------------------
110
79
 
111
- async function login(argv) {
80
+ function login(argv) {
112
81
  // 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
- }
82
+ const userCode = process.env.DXB_USER_CODE || readFlag(argv, "--user-code") || "mock-user-code";
125
83
 
126
84
  // arg 通道:框架通过 fieldInjection 追加 --default-shop
127
85
  const defaultShop =
@@ -129,194 +87,310 @@ async function login(argv) {
129
87
  readFlag(argv, "--default-store-name") ||
130
88
  readFlag(argv, "--store-name") ||
131
89
  process.env.DXB_STORE_NAME ||
90
+ process.env.DXB_DEFAULT_STORE_NAME ||
132
91
  null;
133
92
 
134
- // 账号标识
135
- const accountAlias =
93
+ // 账号标识:用户在弹窗可填 accountAlias,或用 defaultShop 兜底
94
+ const account =
136
95
  readFlag(argv, "--account") ||
137
96
  process.env.DXB_ACCOUNT ||
138
- null;
139
-
140
- // 调用 verify-code 接口校验密文
141
- let verifyResult;
142
- try {
143
- verifyResult = await postJson(VERIFY_URL, { 'openApiKey':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
- }
97
+ defaultShop ||
98
+ "mock-account";
153
99
 
154
- // 校验成功,写凭据文件
155
- const account = accountAlias || verifyResult.accessToken || defaultShop || "unknown";
100
+ // 写凭据文件(D3 契约:顶层明文 account + version)
156
101
  const config = {
157
- account, // 顶层明文,框架读它做卡片标签
102
+ account, // ← 顶层明文,框架读它做卡片标签(稳定契约,不可改名/挪位)
158
103
  userCode,
159
- accessToken: verifyResult.accessToken || null,
160
- refreshToken: verifyResult.refreshToken || null,
161
- defaultShop: defaultShop || verifyResult.defaultShop || null,
162
104
  version: 1
163
105
  };
106
+ if (defaultShop) config.defaultShop = defaultShop;
164
107
 
165
108
  writeConfig(config);
166
109
 
167
- // 不打印敏感值
110
+ // 不打印任何敏感值
168
111
  console.log(JSON.stringify({
169
112
  success: true,
170
113
  connected: true,
171
114
  account: config.account,
172
- defaultShop: config.defaultShop
115
+ defaultShop: config.defaultShop || null
173
116
  }, null, 2));
174
117
  process.exit(0);
175
118
  }
176
119
 
177
120
  // ---------------------------------------------------------------------------
178
- // 授权状态
121
+ // auth status / auth logout
179
122
  // ---------------------------------------------------------------------------
180
123
 
181
124
  function authStatus() {
182
125
  const config = readConfig();
183
- const connected = fs.existsSync(CONFIG_FILE) && Boolean(config.userCode);
184
126
  console.log(JSON.stringify({
185
- connected,
127
+ connected: fs.existsSync(CONFIG_FILE),
186
128
  account: config.account || null,
187
129
  defaultShop: config.defaultShop || null
188
130
  }, null, 2));
189
131
  process.exit(0);
190
132
  }
191
133
 
134
+ function authLogout() {
135
+ if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
136
+ console.log(JSON.stringify({ success: true }, null, 2));
137
+ process.exit(0);
138
+ }
139
+
192
140
  // ---------------------------------------------------------------------------
193
- // 授权验证
194
- // 读取凭据文件 → 调用 verify-code 接口验证密文是否仍然有效
141
+ // 业务命令(mock,不调用真实 API)
195
142
  // ---------------------------------------------------------------------------
196
143
 
197
- async function authVerify(argv) {
144
+ function requireAuth(args) {
198
145
  const config = readConfig();
146
+ const userCode = args.userCode || args.user_code || process.env.DXB_USER_CODE || config.userCode || "mock-user-code";
147
+ return { userCode };
148
+ }
199
149
 
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);
207
- }
208
-
209
- // 支持传入额外 userCode 覆盖
210
- const userCode =
211
- readFlag(argv, "--user-code") ||
212
- process.env.DXB_USER_CODE ||
213
- config.userCode;
150
+ function requireTaskTarget(args) {
151
+ const config = readConfig();
152
+ const ptName = args.ptName;
153
+ const storeName =
154
+ args.storeName ||
155
+ args.defaultShop ||
156
+ args.defaultStoreName ||
157
+ process.env.DXB_STORE_NAME ||
158
+ config.defaultShop ||
159
+ config.defaultStoreName ||
160
+ "mock-store";
161
+ return { ptName, storeName };
162
+ }
214
163
 
215
- try {
216
- const result = await postJson(VERIFY_URL, { 'openApiKey':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);
164
+ async function postJson(baseUrl, pathName, body) {
165
+ const url = `${baseUrl}${pathName}`;
166
+ if (url.includes("/submit")) {
167
+ return { success: true, message: "Task submitted successfully", taskKey: body.taskKey };
236
168
  }
169
+ if (url.includes("/aiReleaseStatus")) {
170
+ return { success: true, planId: body.planId, status: "processing" };
171
+ }
172
+ return { success: true, raw: body };
173
+ }
174
+
175
+ function printResult(data) {
176
+ console.log(JSON.stringify(data, null, 2));
237
177
  }
238
178
 
239
179
  // ---------------------------------------------------------------------------
240
- // 退出授权
180
+ // D4 · stdio MCP server stub
181
+ // 连接成功后框架会执行 npx -y @eprolo/eprolo-cli(无子命令)拉起 MCP
182
+ // stdout 只能输出 MCP 协议帧,调试日志走 stderr
241
183
  // ---------------------------------------------------------------------------
242
184
 
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);
185
+ function startMcpServer() {
186
+ // MCP stub:读凭据文件,通过 stdio 协议响应 tools/list 和 tools/call
187
+ const config = readConfig();
188
+ if (!config.userCode) {
189
+ console.error("未连接或凭据失效,请在 Accio Work 中重新连接");
190
+ process.exit(1);
191
+ }
192
+
193
+ let inputBuffer = "";
194
+ process.stdin.setEncoding("utf8");
195
+ process.stdin.on("data", (chunk) => {
196
+ inputBuffer += chunk;
197
+ // MCP stdio 协议:消息以换行分隔的 JSON-RPC
198
+ let newlineIdx;
199
+ while ((newlineIdx = inputBuffer.indexOf("\n")) !== -1) {
200
+ const line = inputBuffer.slice(0, newlineIdx).trim();
201
+ inputBuffer = inputBuffer.slice(newlineIdx + 1);
202
+ if (line) handleMcpMessage(line);
203
+ }
204
+ });
205
+ process.stdin.on("end", () => process.exit(0));
247
206
  }
248
207
 
249
- // ---------------------------------------------------------------------------
250
- // 帮助
251
- // ---------------------------------------------------------------------------
208
+ function handleMcpMessage(line) {
209
+ let msg;
210
+ try {
211
+ msg = JSON.parse(line);
212
+ } catch (_) {
213
+ return;
214
+ }
252
215
 
253
- function printHelp() {
254
- console.log(`DXB CLI
216
+ const { id, method, params } = msg;
255
217
 
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
- 退出授权,删除凭据文件
218
+ if (method === "initialize") {
219
+ sendMcpResult(id, {
220
+ protocolVersion: "2024-11-05",
221
+ serverInfo: { name: "dxb-mcp", version: "1.0.0" },
222
+ capabilities: { tools: {} }
223
+ });
224
+ return;
225
+ }
265
226
 
266
- Authorization:
267
- 框架通过 fieldInjection 把表单值递交给 CLI:
268
- userCode → env 通道 → process.env.DXB_USER_CODE
269
- defaultShop → arg 通道 → --default-shop
227
+ if (method === "tools/list") {
228
+ sendMcpResult(id, {
229
+ tools: [
230
+ {
231
+ name: "ai_release_products",
232
+ description: "AI 发品:用货源链接在指定店铺下自动发布商品",
233
+ inputSchema: {
234
+ type: "object",
235
+ properties: {
236
+ sourceUrlList: { type: "array", items: { type: "string" }, description: "货源链接列表" },
237
+ storeName: { type: "string", description: "目标授权店铺名(缺省用凭据里的默认店铺)" },
238
+ ptName: { type: "string", description: "目标平台,如 icbu" }
239
+ },
240
+ required: ["sourceUrlList"]
241
+ }
242
+ },
243
+ {
244
+ name: "ai_release_status",
245
+ description: "查询 AI 发品计划状态",
246
+ inputSchema: {
247
+ type: "object",
248
+ properties: {
249
+ planId: { type: "string", description: "发品计划 ID" },
250
+ storeName: { type: "string", description: "目标授权店铺名" }
251
+ },
252
+ required: ["planId"]
253
+ }
254
+ }
255
+ ]
256
+ });
257
+ return;
258
+ }
270
259
 
271
- 连接成功的唯一判定:凭据文件 ~/.dxb-toolkit/config.json 出现
272
- 凭据文件顶层明文 account 字段用于授权卡片标签
260
+ if (method === "tools/call") {
261
+ const { name, arguments: args } = params;
262
+ if (name === "ai_release_products") {
263
+ sendMcpResult(id, {
264
+ content: [{ type: "text", text: JSON.stringify({
265
+ success: true,
266
+ message: "Task submitted successfully",
267
+ taskKey: taskKey("ai_release_products")
268
+ }) }]
269
+ });
270
+ return;
271
+ }
272
+ if (name === "ai_release_status") {
273
+ sendMcpResult(id, {
274
+ content: [{ type: "text", text: JSON.stringify({
275
+ success: true,
276
+ planId: args.planId,
277
+ status: "processing"
278
+ }) }]
279
+ });
280
+ return;
281
+ }
282
+ sendMcpError(id, -32601, `Unknown tool: ${name}`);
283
+ return;
284
+ }
273
285
 
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
- `);
286
+ sendMcpError(id, -32601, `Unknown method: ${method}`);
287
+ }
288
+
289
+ function sendMcpResult(id, result) {
290
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
291
+ }
292
+
293
+ function sendMcpError(id, code, message) {
294
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n");
280
295
  }
281
296
 
282
297
  // ---------------------------------------------------------------------------
283
- // 入口
298
+ // 入口分发(D2 + D4)
299
+ // argv 有 "auth login" → 走 D2 登录
300
+ // 否则默认起 stdio MCP(D4)
284
301
  // ---------------------------------------------------------------------------
285
302
 
286
303
  async function main() {
287
304
  const argv = process.argv.slice(2);
288
- const [group, command] = argv;
289
305
 
290
- if (!group || group === "--help" || group === "-h") {
291
- printHelp();
306
+ // D2 · login
307
+ if (argv[0] === "auth" && argv[1] === "login") {
308
+ login(argv.slice(2));
292
309
  return;
293
310
  }
294
311
 
295
- if (group === "auth" && command === "login") {
296
- await login(argv.slice(2));
312
+ if (argv[0] === "auth" && argv[1] === "status") {
313
+ authStatus();
297
314
  return;
298
315
  }
299
316
 
300
- if (group === "auth" && command === "verify") {
301
- await authVerify(argv.slice(2));
317
+ if (argv[0] === "auth" && argv[1] === "logout") {
318
+ authLogout();
302
319
  return;
303
320
  }
304
321
 
305
- if (group === "auth" && command === "status") {
306
- authStatus();
322
+ // 兼容旧版 CLI 命令(batch submit / status ai)
323
+ if (argv[0] === "batch" && argv[1] === "submit") {
324
+ const args = parseArgs(argv.slice(2));
325
+ if (!args.action) throw new Error("batch submit requires --action");
326
+ if (args.action !== "ai_release_products") {
327
+ throw new Error("Only ai_release_products is supported.");
328
+ }
329
+ const auth = requireAuth(args);
330
+ const target = requireTaskTarget(args);
331
+ const body = {
332
+ action: args.action,
333
+ taskKey: args.taskKey || taskKey(args.action),
334
+ userCode: auth.userCode,
335
+ storeName: target.storeName,
336
+ productIds: splitIds(args.productIds)
337
+ };
338
+ if (target.ptName) body.ptName = target.ptName;
339
+ if (args.dataJson) body.data = JSON.parse(args.dataJson);
340
+ if (args.sourceUrlList) body.sourceUrlList = splitIds(args.sourceUrlList);
341
+ printResult(await postJson("", "/submit", body));
307
342
  return;
308
343
  }
309
344
 
310
- if (group === "auth" && command === "logout") {
311
- authLogout();
345
+ if (argv[0] === "status" && argv[1] === "ai") {
346
+ const args = parseArgs(argv.slice(2));
347
+ if (!args.planId) throw new Error("status ai requires --plan-id");
348
+ const auth = requireAuth(args);
349
+ const target = requireTaskTarget(args);
350
+ printResult(await postJson("", "/aiReleaseStatus", {
351
+ planId: args.planId,
352
+ userCode: auth.userCode,
353
+ storeName: target.storeName
354
+ }));
312
355
  return;
313
356
  }
314
357
 
315
- console.error(JSON.stringify({
316
- success: false,
317
- errorMessage: `Unknown command: ${argv.join(" ")}`
318
- }, null, 2));
319
- process.exit(1);
358
+ if (argv[0] === "--help" || argv[0] === "-h" || argv.length === 0) {
359
+ printHelp();
360
+ return;
361
+ }
362
+
363
+ // D4 · 默认起 stdio MCP server
364
+ startMcpServer();
365
+ }
366
+
367
+ function printHelp() {
368
+ console.log(`DXB CLI (cli-login 形态)
369
+
370
+ Usage:
371
+ eprolo-cli auth login [--default-shop <shop>] 登录并写凭据文件(框架通过 env 注入 DXB_USER_CODE)
372
+ eprolo-cli auth status 查看授权状态
373
+ eprolo-cli auth logout 退出授权
374
+ eprolo-cli batch submit --action ai_release_products --source-url-list <urls> [--store-name <shop>]
375
+ eprolo-cli status ai --plan-id <planId> [--store-name <shop>]
376
+ eprolo-cli 启动 stdio MCP server(框架连接成功后自动拉起)
377
+
378
+ Authorization:
379
+ 框架通过 fieldInjection 把表单值递交给 CLI:
380
+ userCode → env 通道 → process.env.DXB_USER_CODE
381
+ defaultShop → arg 通道 → --default-shop
382
+
383
+ 连接成功的唯一判定:凭据文件 ~/.dxb-toolkit/config.json 出现
384
+ 凭据文件顶层明文 account 字段用于授权卡片标签
385
+
386
+ Credential file (~/.dxb-toolkit/config.json):
387
+ {
388
+ "account": "user@example.com", // 顶层明文,框架读
389
+ "userCode": "...",
390
+ "defaultShop": "shop-001",
391
+ "version": 1
392
+ }
393
+ `);
320
394
  }
321
395
 
322
396
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eprolo-cli",
3
- "version": "1.0.31",
3
+ "version": "1.0.32",
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"