@optima-chat/tiktok-cli 0.12.0 → 0.13.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/dist/index.js +314 -3
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ function authHeaders() {
|
|
|
33
33
|
return {};
|
|
34
34
|
}
|
|
35
35
|
function classify(status, body, shop) {
|
|
36
|
-
const meta = { shop, tiktok_request_id: body?.request_id };
|
|
36
|
+
const meta = { shop, tiktok_request_id: body?.request_id, http_status: status };
|
|
37
37
|
if (status === 0 || body?.code === 0) {
|
|
38
38
|
return { ok: true, data: body.data, meta };
|
|
39
39
|
}
|
|
@@ -766,6 +766,72 @@ var COMMANDS = [
|
|
|
766
766
|
},
|
|
767
767
|
readback: ({ shop, args: args2 }) => proxy("POST", shop, "/promotion/202309/activities/search", { body: { status: "NOT_START", page_size: 20 } })
|
|
768
768
|
},
|
|
769
|
+
{
|
|
770
|
+
name: "promotions create-discount",
|
|
771
|
+
summary: "\u5EFA\u5546\u54C1\u6298\u6263\u6D3B\u52A8 FIXED_PRICE\uFF08--title [--product] --price <skuId>:<\u4E00\u53E3\u4EF7>|<productId>/<skuId>:<\u4E00\u53E3\u4EF7>[,...] [--days 180]\uFF09\u3002\u4E24\u6B65:\u5EFA\u58F3+\u52A0\u54C1\uFF0C\u53EF\u591A\u5546\u54C1",
|
|
772
|
+
write: true,
|
|
773
|
+
validate: validateCreateDiscount,
|
|
774
|
+
preview: ({ args: args2 }) => {
|
|
775
|
+
const { begin, end } = discountWindow(args2);
|
|
776
|
+
return [
|
|
777
|
+
{ step: "1/2 \u5EFA\u6D3B\u52A8\u58F3", method: "POST", path: "/promotion/202309/activities", body: discountActivityBody(args2, begin, end) },
|
|
778
|
+
{ step: "2/2 \u52A0\u5546\u54C1+\u4E00\u53E3\u4EF7", method: "PUT", path: "/promotion/202309/activities/{activity_id}/products", body: discountProductsBody(args2), human: discountHuman(args2) }
|
|
779
|
+
];
|
|
780
|
+
},
|
|
781
|
+
run: async ({ shop, args: args2 }) => {
|
|
782
|
+
const { begin, end } = discountWindow(args2);
|
|
783
|
+
const submitted = submittedSkus(args2);
|
|
784
|
+
const created = await proxy("POST", shop, "/promotion/202309/activities", { body: discountActivityBody(args2, begin, end) });
|
|
785
|
+
if (!created.ok) {
|
|
786
|
+
const err = created.error;
|
|
787
|
+
switch (createFailureVerdict(created)) {
|
|
788
|
+
case "maybe_landed":
|
|
789
|
+
return { ...created, error: { ...err, message: `${err.message} ${DISCOUNT_TEXT.maybeLanded}` } };
|
|
790
|
+
case "dup":
|
|
791
|
+
return { ...created, error: { ...err, message: `${err.message} ${await dupSelfCheck(shop, args2.title, submitted)}` } };
|
|
792
|
+
case "rejected":
|
|
793
|
+
return created;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const activityId = created.data?.activity_id;
|
|
797
|
+
if (!activityId) {
|
|
798
|
+
return {
|
|
799
|
+
ok: false,
|
|
800
|
+
error: { code: "TIKTOK_ERROR", message: DISCOUNT_TEXT.noCreateId(args2.title), retryable: false },
|
|
801
|
+
meta: created.meta
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
const withProducts = await proxy("PUT", shop, `/promotion/202309/activities/${activityId}/products`, {
|
|
805
|
+
body: { ...discountProductsBody(args2), activity_id: activityId }
|
|
806
|
+
// F5:body 里要重复传
|
|
807
|
+
});
|
|
808
|
+
const rb = await proxy("GET", shop, `/promotion/202309/activities/${activityId}`, {});
|
|
809
|
+
const readback = rb.ok ? rb.data : { error: rb.error };
|
|
810
|
+
if (!withProducts.ok) {
|
|
811
|
+
const { retry_after_s: _dropped, ...err } = withProducts.error;
|
|
812
|
+
let note;
|
|
813
|
+
const c = rb.ok ? compareReadback(submitted, rb.data) : null;
|
|
814
|
+
if (!c) note = DISCOUNT_TEXT.addFailReadbackFailed(shop, activityId);
|
|
815
|
+
else {
|
|
816
|
+
if (c.missing.length === 0 && c.mismatched.length === 0) note = DISCOUNT_TEXT.addFailAllAttached(submitted.length, activityId);
|
|
817
|
+
else if (c.missing.length === submitted.length && c.productCount === 0) note = DISCOUNT_TEXT.addFailNoneAttached(shop, activityId);
|
|
818
|
+
else note = DISCOUNT_TEXT.addFailPartial(problemsText(c), shop, activityId);
|
|
819
|
+
}
|
|
820
|
+
return { ok: false, error: { ...err, retryable: false }, data: { activity_id: activityId, readback, note }, meta: withProducts.meta };
|
|
821
|
+
}
|
|
822
|
+
const ours = [];
|
|
823
|
+
const diff = rb.ok ? compareReadback(submitted, rb.data) : null;
|
|
824
|
+
if (!diff) ours.push(DISCOUNT_TEXT.readbackFailed(activityId));
|
|
825
|
+
else {
|
|
826
|
+
const problems = problemsText(diff);
|
|
827
|
+
if (problems) ours.push(DISCOUNT_TEXT.readbackDiffers(activityId, problems));
|
|
828
|
+
}
|
|
829
|
+
const putData = withProducts.data ?? {};
|
|
830
|
+
const data = { ...putData, activity_id: activityId, readback };
|
|
831
|
+
if (ours.length > 0) data.warnings = mergeWarnings(putData.warnings, ours);
|
|
832
|
+
return { ok: true, data, meta: withProducts.meta };
|
|
833
|
+
}
|
|
834
|
+
},
|
|
769
835
|
{
|
|
770
836
|
name: "promotions deactivate",
|
|
771
837
|
summary: "\u7EC8\u6B62/\u505C\u7528\u4FC3\u9500\u6D3B\u52A8\uFF08--id \u5FC5\u586B\uFF09\u3002\u4E0D\u53EF\u9006",
|
|
@@ -1181,6 +1247,246 @@ function findCommand(tokens) {
|
|
|
1181
1247
|
}
|
|
1182
1248
|
return null;
|
|
1183
1249
|
}
|
|
1250
|
+
var DISCOUNT_FLAGS = ["shop", "apply", "actor", "title", "product", "price", "days"];
|
|
1251
|
+
var PRICE_ITEM = /^(?:([^:,\s/]+)\/)?([^:,\s/]+):(0|[1-9]\d*)(?:\.(\d{1,2}))?$/;
|
|
1252
|
+
function parsePriceItems(raw, defaultProduct) {
|
|
1253
|
+
const items = [];
|
|
1254
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1255
|
+
for (const part of raw.split(",")) {
|
|
1256
|
+
const m = PRICE_ITEM.exec(part);
|
|
1257
|
+
if (!m) {
|
|
1258
|
+
return { ok: false, message: `--price \u6BCF\u4E00\u9879\u5FC5\u987B\u662F <skuId>:<\u4E00\u53E3\u4EF7> \u6216 <productId>/<skuId>:<\u4E00\u53E3\u4EF7>\uFF08\u5341\u8FDB\u5236\u6570\u5B57\uFF0C\u6700\u591A\u4E24\u4F4D\u5C0F\u6570\u3001\u4E0D\u8981\u524D\u5BFC\u96F6\uFF0C\u5982 25.42\uFF09\uFF0C\u591A\u9879\u7528\u9017\u53F7\u8FDE\u63A5\u3001\u5404\u9879\u4E4B\u95F4\u4E0D\u8981\u6709\u7A7A\u683C\uFF0C\u5982 --price 173707:29.90,173708:32.00\uFF1B\u6536\u5230\u300C${part}\u300D` };
|
|
1259
|
+
}
|
|
1260
|
+
const [, explicitPid, sid, intPart, frac] = m;
|
|
1261
|
+
if (explicitPid && defaultProduct && explicitPid !== defaultProduct) {
|
|
1262
|
+
return { ok: false, message: `--price \u7684\u300C${part}\u300D\u6307\u5B9A\u4E86\u5546\u54C1 ${explicitPid}\uFF0C\u4E0E --product ${defaultProduct} \u4E0D\u4E00\u81F4\uFF1B\u591A\u5546\u54C1\u8BF7\u628A --product \u53BB\u6389\u3001\u6BCF\u9879\u90FD\u5199\u6210 <productId>/<skuId>:<\u4EF7>` };
|
|
1263
|
+
}
|
|
1264
|
+
const pid = explicitPid ?? defaultProduct;
|
|
1265
|
+
if (!pid) {
|
|
1266
|
+
return { ok: false, message: `--price \u7684\u300C${part}\u300D\u6CA1\u5E26 productId\uFF0C\u800C\u4F60\u4E5F\u6CA1\u4F20 --product\uFF1B\u8981\u4E48\u52A0 --product\uFF0C\u8981\u4E48\u5199\u6210 <productId>/<skuId>:<\u4EF7>` };
|
|
1267
|
+
}
|
|
1268
|
+
const amount = frac === void 0 ? intPart : `${intPart}.${frac}`;
|
|
1269
|
+
if (!(Number(amount) > 0)) return { ok: false, message: `--price \u7684\u4E00\u53E3\u4EF7\u5FC5\u987B\u5927\u4E8E 0\uFF1B\u6536\u5230\u300C${part}\u300D` };
|
|
1270
|
+
if (seen.has(sid)) return { ok: false, message: `--price \u91CC\u540C\u4E00\u4E2A skuId \u91CD\u590D\u51FA\u73B0\uFF1A${sid}` };
|
|
1271
|
+
seen.add(sid);
|
|
1272
|
+
items.push({ pid, sid, amount });
|
|
1273
|
+
}
|
|
1274
|
+
return { ok: true, items };
|
|
1275
|
+
}
|
|
1276
|
+
function discountWindow(args2) {
|
|
1277
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1278
|
+
const begin = now + 900;
|
|
1279
|
+
const end = begin + Number(args2.days ?? 180) * 86400;
|
|
1280
|
+
return { begin, end };
|
|
1281
|
+
}
|
|
1282
|
+
function discountActivityBody(args2, begin, end) {
|
|
1283
|
+
return {
|
|
1284
|
+
title: args2.title,
|
|
1285
|
+
activity_type: "FIXED_PRICE",
|
|
1286
|
+
product_level: "VARIATION",
|
|
1287
|
+
begin_time: begin,
|
|
1288
|
+
end_time: end
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
function discountProductsBody(args2) {
|
|
1292
|
+
const parsed = parsePriceItems(args2.price ?? "", args2.product);
|
|
1293
|
+
const items = parsed.ok ? parsed.items : [];
|
|
1294
|
+
const order = [];
|
|
1295
|
+
const byPid = /* @__PURE__ */ new Map();
|
|
1296
|
+
for (const it of items) {
|
|
1297
|
+
if (!byPid.has(it.pid)) {
|
|
1298
|
+
byPid.set(it.pid, []);
|
|
1299
|
+
order.push(it.pid);
|
|
1300
|
+
}
|
|
1301
|
+
byPid.get(it.pid).push({ sid: it.sid, amount: it.amount });
|
|
1302
|
+
}
|
|
1303
|
+
return {
|
|
1304
|
+
products: order.map((pid) => ({
|
|
1305
|
+
id: pid,
|
|
1306
|
+
quantity_limit: -1,
|
|
1307
|
+
quantity_per_user: -1,
|
|
1308
|
+
skus: byPid.get(pid).map((it) => ({
|
|
1309
|
+
id: it.sid,
|
|
1310
|
+
activity_price_amount: it.amount,
|
|
1311
|
+
// 🔴 写侧裸串;读回叫 activity_price.amount(F4)
|
|
1312
|
+
quantity_limit: -1,
|
|
1313
|
+
quantity_per_user: -1
|
|
1314
|
+
}))
|
|
1315
|
+
}))
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
function discountHuman(args2) {
|
|
1319
|
+
const parsed = parsePriceItems(args2.price ?? "", args2.product);
|
|
1320
|
+
const days = Number(args2.days ?? 180);
|
|
1321
|
+
const list = parsed.ok ? parsed.items.map((it) => `\u5546\u54C1 ${it.pid} \u7684 SKU ${it.sid} \u552E\u4EF7\u5B9A\u4E3A ${it.amount}`).join("\uFF1B") : "\uFF08--price \u89E3\u6790\u5931\u8D25\uFF09";
|
|
1322
|
+
return `${list}\u3002\u539F\u4EF7\u5C06\u663E\u793A\u4E3A\u5212\u7EBF\u4EF7\u3002\u6D3B\u52A8\u5171 ${days} \u5929\uFF0C\u4ECE 15 \u5206\u949F\u540E\u5F00\u59CB \u2014\u2014 \u521A\u5EFA\u5B8C\u53BB\u5546\u54C1\u9875\u770B\u4EF7\u683C\u6CA1\u53D8\u662F\u6B63\u5E38\u7684\uFF0C\u90A3\u65F6\u6D3B\u52A8\u8FD8\u662F NOT_START\u3002`;
|
|
1323
|
+
}
|
|
1324
|
+
function validateCreateDiscount(ctx2) {
|
|
1325
|
+
const { args: args2, rest } = ctx2;
|
|
1326
|
+
for (const k of Object.keys(args2)) {
|
|
1327
|
+
if (DISCOUNT_FLAGS.includes(k)) continue;
|
|
1328
|
+
if (k === "percent") {
|
|
1329
|
+
return { ok: false, message: "\u672C\u7248\u4E0D\u652F\u6301\u767E\u5206\u6BD4\u6298\u6263\uFF08--percent\uFF09\u3002\u8BF7\u5148\u7528 tiktok products get --shop <id> --product <pid> \u53D6 SKU \u6302\u724C\u4EF7\uFF0C\u81EA\u884C\u6298\u7B97\uFF08\u4FDD\u7559\u4E24\u4F4D\u5C0F\u6570\uFF09\u540E\u8D70 --price <skuId>:<\u6298\u540E\u4EF7>" };
|
|
1330
|
+
}
|
|
1331
|
+
if (k === "sku") {
|
|
1332
|
+
return { ok: false, message: "\u672C\u547D\u4EE4\u6CA1\u6709 --sku\uFF1AskuId \u5199\u5728 --price \u91CC\uFF0C\u5982 --price 173707:29.90\uFF08\u6CA1\u4F20 --product \u65F6\u6BCF\u9879\u90FD\u8981\u5E26 productId\uFF0C\u5982 --price 1737048/173707:29.90\uFF09" };
|
|
1333
|
+
}
|
|
1334
|
+
return { ok: false, message: `\u4E0D\u652F\u6301\u7684\u53C2\u6570 --${k}\uFF1B\u672C\u547D\u4EE4\u53EA\u8BA4 ${DISCOUNT_FLAGS.map((f) => "--" + f).join(" ")}` };
|
|
1335
|
+
}
|
|
1336
|
+
if (rest && rest.length > 0) {
|
|
1337
|
+
return { ok: false, message: `\u591A\u4F59\u7684\u4F4D\u7F6E\u53C2\u6570\u300C${rest.join(" ")}\u300D\u2014\u2014\u6BCF\u4E2A\u53C2\u6570\u53EA\u80FD\u5E26\u4E00\u4E2A\u503C\u3002\u591A\u4E2A SKU \u8981\u7528\u9017\u53F7\u8FDE\u6210\u4E00\u4E2A --price\uFF0C\u5982 --price A:12,B:15\uFF08\u4E0D\u80FD\u7528\u7A7A\u683C\u5206\u9694\uFF09\uFF1B\u82E5\u662F\u6807\u9898\u7B49\u503C\u91CC\u542B\u7A7A\u683C\uFF0C\u7ED9\u6574\u4E2A\u503C\u52A0\u5F15\u53F7\uFF0C\u5982 --title "Momo Sale"` };
|
|
1338
|
+
}
|
|
1339
|
+
for (const k of ["title", "price"]) {
|
|
1340
|
+
const v = args2[k];
|
|
1341
|
+
if (!v || v === "true" || !v.trim()) {
|
|
1342
|
+
return { ok: false, message: `--${k} \u5FC5\u586B\uFF0C\u4E14\u5FC5\u987B\u5E26\u503C\uFF08\u53EA\u5199 --${k} \u4E0D\u7ED9\u503C\u4F1A\u88AB\u89E3\u6790\u6210\u5B57\u7B26\u4E32 "true"\uFF0C\u5199\u6210 --${k}= \u5219\u662F\u7A7A\u503C\uFF0C\u4E24\u79CD\u90FD\u4E0D\u7B97\uFF09` };
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
if (args2.title !== args2.title.trim()) {
|
|
1346
|
+
return { ok: false, message: `--title \u9996\u5C3E\u4E0D\u80FD\u6709\u7A7A\u767D\uFF08\u6536\u5230\u300C${args2.title}\u300D\uFF09\uFF1A\u649E\u91CD\u540D\u65F6 CLI \u6309\u6807\u9898\u4E25\u683C\u76F8\u7B49\u67E5\u540C\u540D\u6D3B\u52A8\uFF0C\u5E73\u53F0\u82E5\u53BB\u6389\u9996\u5C3E\u7A7A\u767D\u5C31\u4F1A\u5BF9\u4E0D\u4E0A\u3002\u8BF7\u53BB\u6389\u9996\u5C3E\u7A7A\u767D\u540E\u91CD\u8DD1` };
|
|
1347
|
+
}
|
|
1348
|
+
if ("product" in args2 && (!args2.product || args2.product === "true")) {
|
|
1349
|
+
return { ok: false, message: `--product \u5FC5\u586B\uFF0C\u4E14\u5FC5\u987B\u5E26\u503C\uFF08\u53EA\u5199 --product \u4E0D\u7ED9\u503C\u4F1A\u88AB\u89E3\u6790\u6210\u5B57\u7B26\u4E32 "true"\uFF0C\u5199\u6210 --product= \u5219\u662F\u7A7A\u503C\uFF0C\u4E24\u79CD\u90FD\u4E0D\u7B97\uFF09` };
|
|
1350
|
+
}
|
|
1351
|
+
if (args2.product?.includes(",")) {
|
|
1352
|
+
return { ok: false, message: "--product \u53EA\u6536\u4E00\u4E2A\u5546\u54C1 id\uFF1B\u591A\u5546\u54C1\u8BF7\u5728 --price \u91CC\u5199 <productId>/<skuId>:<\u4EF7>" };
|
|
1353
|
+
}
|
|
1354
|
+
const parsed = parsePriceItems(args2.price, args2.product);
|
|
1355
|
+
if (!parsed.ok) return parsed;
|
|
1356
|
+
if ("days" in args2) {
|
|
1357
|
+
const raw = args2.days;
|
|
1358
|
+
const n = Number(raw);
|
|
1359
|
+
if (!/^\d+$/.test(raw) || !(n >= 1 && n <= 180)) {
|
|
1360
|
+
return { ok: false, message: `--days \u5FC5\u987B\u662F 1\u2013180 \u7684\u6574\u6570\uFF08\u6536\u5230\u300C${raw}\u300D\uFF09` };
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return { ok: true };
|
|
1364
|
+
}
|
|
1365
|
+
var deactivateCmd = (shop, id) => `\`tiktok promotions deactivate --shop ${shop} --id ${id} --apply\``;
|
|
1366
|
+
var DISCOUNT_TEXT = {
|
|
1367
|
+
// ⑤ 建壳可能已落。标题 = 幂等键:原标题重跑,最坏撞一次重名,不会多出一条活动。
|
|
1368
|
+
maybeLanded: "\u26A0\uFE0F \u5EFA\u58F3\u8BF7\u6C42\u53EF\u80FD\u5DF2\u7ECF\u843D\u5230\u5E73\u53F0\u2014\u2014\u522B\u6362\u6807\u9898\uFF0C\u76F4\u63A5\u7528\u539F\u6807\u9898\u91CD\u8DD1\u539F\u547D\u4EE4\uFF1A\u6CA1\u843D\u5C31\u4F1A\u6B63\u5E38\u5EFA\u6210\uFF1B\u82E5\u5DF2\u843D\uFF0C\u91CD\u8DD1\u4F1A\u649E\u91CD\u540D\uFF0C\u90A3\u6761\u63D0\u793A\u4F1A\u544A\u8BC9\u4F60\u4E0B\u4E00\u6B65\u3002",
|
|
1369
|
+
// ⑥
|
|
1370
|
+
noCreateId: (title) => `\u5EFA\u58F3\u8FD4\u56DE\u6210\u529F\u4F46\u6CA1\u62FF\u5230 activity_id \u2014\u2014 \u26A0\uFE0F \u5E73\u53F0\u4E0A\u5F88\u53EF\u80FD\u5DF2\u7ECF\u6709\u8FD9\u6761\u58F3\u4E86\uFF08\u6807\u9898\u300C${title}\u300D\uFF09\u3002\u522B\u6362\u6807\u9898\uFF0C\u76F4\u63A5\u7528\u539F\u6807\u9898\u91CD\u8DD1\u539F\u547D\u4EE4\uFF1A\u91CD\u8DD1\u4F1A\u649E\u91CD\u540D\uFF0C\u90A3\u6761\u63D0\u793A\u4F1A\u544A\u8BC9\u4F60\u4E0B\u4E00\u6B65\u3002`,
|
|
1371
|
+
// ⑦ 撞重名后自查的六种结论(前面由调用方拼上平台原文 + 一个半角空格)
|
|
1372
|
+
dupSame: (title, aid, status) => `\u26A0\uFE0F \u540C\u540D\u6D3B\u52A8\u300C${title}\u300D\u5DF2\u5B58\u5728\uFF08\u6D3B\u52A8 ${aid}\uFF0C${status}\uFF09\uFF0C\u6302\u7684\u5546\u54C1\u4E0E\u672C\u6B21\u63D0\u4EA4\u5B8C\u5168\u4E00\u81F4\u2014\u2014\u8FD9\u5C31\u662F\u4F60\u8981\u7684\u6D3B\u52A8\uFF0C\u5DF2\u7ECF\u5EFA\u597D\u4E86\uFF1A\u522B\u91CD\u5EFA\u3002`,
|
|
1373
|
+
dupEmpty: (title, aid, status, shop) => `\u26A0\uFE0F \u540C\u540D\u6D3B\u52A8\u300C${title}\u300D\u5DF2\u5B58\u5728\uFF08\u6D3B\u52A8 ${aid}\uFF0C${status}\uFF09\uFF0C\u4F46\u5B83\u662F\u4E00\u6761\u7A7A\u58F3\uFF08\u6CA1\u6709\u4EFB\u4F55\u5546\u54C1\uFF09\u2014\u2014\u5148\u6E05\u7406\uFF1A${deactivateCmd(shop, aid)}\uFF0C\u518D\u7528\u539F\u6807\u9898\u91CD\u8DD1\u539F\u547D\u4EE4\uFF1B\u91CD\u8DD1\u82E5\u53C8\u649E\u91CD\u540D\uFF0C\u6362\u4E2A\u6807\u9898\u3002`,
|
|
1374
|
+
dupOwner: (title, aid, status, problems) => `\u26A0\uFE0F \u540C\u540D\u6D3B\u52A8\u300C${title}\u300D\u5DF2\u5B58\u5728\uFF08\u6D3B\u52A8 ${aid}\uFF0C${status}\uFF09\uFF0C\u6302\u7684\u5546\u54C1\u4E0E\u672C\u6B21\u63D0\u4EA4\u4E0D\u4E00\u81F4\uFF1A${problems}\u2014\u2014\u522B\u52A8\u5B83\uFF0C\u4E5F\u5148\u522B\u91CD\u5EFA\uFF1A\u628A\u5B83\u544A\u8BC9\u5E97\u4E3B\uFF0C\u95EE\u662F\u5C31\u7528\u8FD9\u6761\uFF0C\u8FD8\u662F\u6362\u4E2A\u6807\u9898\u53E6\u5EFA\u3002`,
|
|
1375
|
+
dupRetitle: (title) => `\u26A0\uFE0F \u672A\u5F00\u59CB / \u8FDB\u884C\u4E2D\u7684\u6D3B\u52A8\u91CC\u6CA1\u6709\u300C${title}\u300D\u2014\u2014\u8FD9\u4E2A\u6807\u9898\u88AB\u4E00\u6761\u5DF2\u7EC8\u6B62\u6216\u5DF2\u8FC7\u671F\u7684\u6D3B\u52A8\u5360\u7740\uFF0C\u6362\u4E2A\u6807\u9898\u91CD\u8DD1\u539F\u547D\u4EE4\u3002`,
|
|
1376
|
+
dupUnclear: (title, queryError) => `\u26A0\uFE0F \u540C\u540D\u6D3B\u52A8\u300C${title}\u300D\u5DF2\u5B58\u5728\uFF0C\u4F46\u6CA1\u80FD\u67E5\u6E05\u5B83\u7684\u72B6\u6001\uFF08${queryError}\uFF09\u2014\u2014\u522B\u6362\u6807\u9898\u3001\u522B\u91CD\u5EFA\uFF0C\u7A0D\u540E\u7528\u539F\u6807\u9898\u91CD\u8DD1\u539F\u547D\u4EE4\u518D\u770B\uFF1B\u8FDE\u7EED\u4E24\u6B21\u90FD\u662F\u8FD9\u53E5\uFF0C\u5C31\u5982\u5B9E\u544A\u8BC9\u5E97\u4E3B\u300C\u6D3B\u52A8\u53EF\u80FD\u5DF2\u5EFA\uFF0C\u4F46\u67E5\u4E0D\u6E05\u300D\u3002`,
|
|
1377
|
+
dupMulti: (title, found) => `\u26A0\uFE0F \u672A\u5F00\u59CB / \u8FDB\u884C\u4E2D\u7684\u6D3B\u52A8\u91CC\u6709 ${found.length} \u6761\u53EB\u300C${title}\u300D\uFF08${found.map((a) => `${a.id}\uFF1A${a.status}`).join("\uFF1B")}\uFF09\u2014\u2014\u522B\u52A8\u5B83\u4EEC\uFF0C\u4E5F\u5148\u522B\u91CD\u5EFA\uFF1A\u628A\u5B83\u4EEC\u544A\u8BC9\u5E97\u4E3B\uFF0C\u95EE\u600E\u4E48\u5904\u7406\u3002`,
|
|
1378
|
+
// ⑧ 加品失败后按回读下结论
|
|
1379
|
+
addFailAllAttached: (n, id) => `\u52A0\u5546\u54C1\u62A5\u4E86\u9519\uFF0C\u4F46\u56DE\u8BFB\u663E\u793A\u63D0\u4EA4\u7684 ${n} \u4E2A SKU \u90FD\u5DF2\u6309\u4EF7\u6302\u4E0A\uFF08\u6D3B\u52A8 ${id} \u7EA6 15 \u5206\u949F\u540E\u5F00\u59CB\uFF09\u2014\u2014\u522B\u6E05\u7406\uFF0C\u4E5F\u522B\u91CD\u8DD1\u672C\u547D\u4EE4\u3002`,
|
|
1380
|
+
addFailNoneAttached: (shop, id) => `\u52A0\u5546\u54C1\u5931\u8D25\uFF0C\u56DE\u8BFB\u663E\u793A\u6CA1\u6709\u4EFB\u4F55\u5546\u54C1\u6302\u4E0A\u2014\u2014\u6D3B\u52A8 ${id} \u662F\u4E00\u6761\u7A7A\u58F3\u3002\u5148\u6E05\u7406\uFF1A${deactivateCmd(shop, id)}\uFF1B\u518D\u6309\u4E0A\u9762\u7684\u5E73\u53F0\u62A5\u9519\u6539\u6B63\u53C2\u6570\u540E\u91CD\u8DD1\u539F\u547D\u4EE4\uFF08\u53EA\u6709\u9650\u6D41\u3001\u7F51\u7EDC\u7C7B\u62A5\u9519\u624D\u539F\u6837\u91CD\u8DD1\uFF09\u3002\u522B\u76F4\u63A5\u91CD\u8DD1\uFF1A\u4F1A\u649E\u91CD\u540D\u6216\u518D\u5EFA\u4E00\u4E2A\u58F3\u3002`,
|
|
1381
|
+
addFailPartial: (problems, shop, id) => `\u52A0\u5546\u54C1\u62A5\u4E86\u9519\uFF0C\u56DE\u8BFB\u663E\u793A\u6CA1\u6709\u5168\u90E8\u6302\u4E0A\uFF1A${problems}\u3002\u672C\u547D\u4EE4\u4E0D\u80FD\u5F80\u5DF2\u6709\u6D3B\u52A8\u8865\u5546\u54C1\uFF0C\u8BF7\u5E97\u4E3B\u4E8C\u9009\u4E00\uFF1A\u4FDD\u7559\u6D3B\u52A8 ${id}\uFF08\u7F3A\u7684 SKU \u4E0D\u6253\u6298\uFF0C\u4EF7\u683C\u4E0D\u7B26\u7684\u6309\u8BFB\u56DE\u7684\u4EF7\u683C\u751F\u6548\uFF09\uFF0C\u6216\u6E05\u7406\u540E\u6574\u6761\u91CD\u5EFA\uFF1A${deactivateCmd(shop, id)}\uFF0C\u6E05\u7406\u540E\u6309\u4E0A\u9762\u7684\u5E73\u53F0\u62A5\u9519\u6539\u6B63\u53C2\u6570\u518D\u91CD\u8DD1\u539F\u547D\u4EE4\u3002\u522B\u76F4\u63A5\u91CD\u8DD1\u672C\u547D\u4EE4\u3002`,
|
|
1382
|
+
addFailReadbackFailed: (shop, id) => `\u52A0\u5546\u54C1\u5931\u8D25\uFF0C\u56DE\u8BFB\u4E5F\u5931\u8D25\u4E86\uFF0C\u67E5\u4E0D\u5230\u6D3B\u52A8 ${id} \u91CC\u5546\u54C1\u6709\u6CA1\u6709\u6302\u4E0A\u3002\u522B\u91CD\u8DD1\u672C\u547D\u4EE4\uFF08\u4F1A\u518D\u5EFA\u4E00\u4E2A\u58F3\uFF09\uFF0C\u4E5F\u5148\u522B\u6E05\u7406\uFF1A\u5982\u5B9E\u544A\u8BC9\u5E97\u4E3B\u300C\u6D3B\u52A8\u5DF2\u5EFA\uFF0C\u5546\u54C1\u662F\u5426\u6302\u4E0A\u672A\u80FD\u786E\u8BA4\u300D\uFF0C\u8BF7\u5E97\u4E3B\u5728\u5356\u5BB6\u540E\u53F0\u770B\u8FD9\u6761\u6D3B\u52A8\u91CC\u6709\u6CA1\u6709\u5546\u54C1\uFF1B\u786E\u8BA4\u6CA1\u6302\u4E0A\u518D\u6E05\u7406\uFF1A${deactivateCmd(shop, id)}\u3002`,
|
|
1383
|
+
// ⑨ 成功路径的 warnings
|
|
1384
|
+
readbackFailed: (id) => `\u6D3B\u52A8 ${id} \u5DF2\u5EFA\u6210\u3001\u5546\u54C1\u5DF2\u63D0\u4EA4\uFF0C\u4F46\u56DE\u8BFB\u5931\u8D25\uFF0C\u672A\u80FD\u9A8C\u8BC1\u5546\u54C1\u662F\u5426\u6302\u4E0A\u2014\u2014\u4E0D\u8981\u91CD\u5EFA\u6D3B\u52A8\uFF1B\u5982\u5B9E\u544A\u8BC9\u5E97\u4E3B\u300C\u5DF2\u5EFA\uFF0C\u5546\u54C1\u662F\u5426\u6302\u4E0A\u672A\u80FD\u786E\u8BA4\u300D\u3002`,
|
|
1385
|
+
readbackDiffers: (id, problems) => `\u6D3B\u52A8 ${id} \u5DF2\u5EFA\u6210\uFF0C\u4F46\u56DE\u8BFB\u4E0E\u63D0\u4EA4\u4E0D\u4E00\u81F4\uFF1A${problems}\u2014\u2014\u4E0D\u8981\u91CD\u5EFA\u6D3B\u52A8\uFF1B\u5982\u5B9E\u544A\u8BC9\u5E97\u4E3B\u54EA\u4E9B SKU \u672A\u786E\u8BA4\u6302\u4E0A\u3002`
|
|
1386
|
+
};
|
|
1387
|
+
var OBSERVED_REJECT_CODES = /* @__PURE__ */ new Set([
|
|
1388
|
+
36009004,
|
|
1389
|
+
36009009,
|
|
1390
|
+
36009010,
|
|
1391
|
+
17029001,
|
|
1392
|
+
17029004,
|
|
1393
|
+
17029010,
|
|
1394
|
+
17029021,
|
|
1395
|
+
17029022,
|
|
1396
|
+
17029043,
|
|
1397
|
+
17029044,
|
|
1398
|
+
17029063,
|
|
1399
|
+
105002,
|
|
1400
|
+
106001,
|
|
1401
|
+
105005
|
|
1402
|
+
]);
|
|
1403
|
+
var DUP_NAME_CODE = 17029004;
|
|
1404
|
+
function createFailureVerdict(env2) {
|
|
1405
|
+
const err = env2.error;
|
|
1406
|
+
const status = env2.meta?.http_status;
|
|
1407
|
+
if (status === void 0) return "maybe_landed";
|
|
1408
|
+
if (err.code === "RATE_LIMITED") return "maybe_landed";
|
|
1409
|
+
if (status >= 500) return "maybe_landed";
|
|
1410
|
+
const m = err.code === "TIKTOK_ERROR" ? /^\[(\d+)\]/.exec(err.message) : null;
|
|
1411
|
+
const code = m ? Number(m[1]) : void 0;
|
|
1412
|
+
if (status >= 400) return code === DUP_NAME_CODE ? "dup" : "rejected";
|
|
1413
|
+
if (err.code === "AUTH_EXPIRED") return "rejected";
|
|
1414
|
+
if (code !== void 0 && OBSERVED_REJECT_CODES.has(code)) return code === DUP_NAME_CODE ? "dup" : "rejected";
|
|
1415
|
+
return "maybe_landed";
|
|
1416
|
+
}
|
|
1417
|
+
function submittedSkus(args2) {
|
|
1418
|
+
return discountProductsBody(args2).products.flatMap((p) => p.skus.map((s) => ({ sid: s.id, amount: s.activity_price_amount })));
|
|
1419
|
+
}
|
|
1420
|
+
function compareReadback(submitted, readback) {
|
|
1421
|
+
const products = readback?.products;
|
|
1422
|
+
if (!Array.isArray(products)) return null;
|
|
1423
|
+
const skus = products.flatMap((p) => Array.isArray(p?.skus) ? p.skus : []);
|
|
1424
|
+
const mine = new Set(submitted.map((s) => s.sid));
|
|
1425
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1426
|
+
const extra = [];
|
|
1427
|
+
for (const s of skus) {
|
|
1428
|
+
const id = String(s?.id);
|
|
1429
|
+
if ((byId.has(id) || !mine.has(id)) && !extra.includes(id)) extra.push(id);
|
|
1430
|
+
if (!byId.has(id)) byId.set(id, s);
|
|
1431
|
+
}
|
|
1432
|
+
const diff = { missing: [], mismatched: [], extra, total: skus.length, productCount: products.length };
|
|
1433
|
+
for (const { sid, amount } of submitted) {
|
|
1434
|
+
const hit = byId.get(sid);
|
|
1435
|
+
if (!hit) {
|
|
1436
|
+
diff.missing.push(sid);
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
const raw = hit.activity_price?.amount;
|
|
1440
|
+
const got = raw === void 0 || raw === null || raw === "" ? void 0 : String(raw);
|
|
1441
|
+
if (got === void 0 || Number(got) !== Number(amount)) diff.mismatched.push({ sid, submitted: amount, readback: got });
|
|
1442
|
+
}
|
|
1443
|
+
return diff;
|
|
1444
|
+
}
|
|
1445
|
+
function problemsText(d) {
|
|
1446
|
+
const parts = [];
|
|
1447
|
+
if (d.missing.length) parts.push(`\u7F3A ${d.missing.join("\u3001")}`);
|
|
1448
|
+
if (d.mismatched.length) {
|
|
1449
|
+
parts.push(`\u91D1\u989D\u4E0D\u7B26 ${d.mismatched.map((m) => `${m.sid}\uFF08\u63D0\u4EA4 ${m.submitted}\uFF0C\u8BFB\u56DE ${m.readback ?? "\u65E0\u91D1\u989D"}\uFF09`).join("\u3001")}`);
|
|
1450
|
+
}
|
|
1451
|
+
return parts.join("\uFF1B");
|
|
1452
|
+
}
|
|
1453
|
+
function mergeWarnings(existing, ours) {
|
|
1454
|
+
if (existing === null || existing === void 0) return ours;
|
|
1455
|
+
if (Array.isArray(existing)) return [...existing, ...ours];
|
|
1456
|
+
return [existing, ...ours];
|
|
1457
|
+
}
|
|
1458
|
+
var DUP_SEARCH_MAX_PAGES = 10;
|
|
1459
|
+
async function dupSelfCheck(shop, title, submitted) {
|
|
1460
|
+
const found = /* @__PURE__ */ new Map();
|
|
1461
|
+
for (const status of ["NOT_START", "ONGOING"]) {
|
|
1462
|
+
let token;
|
|
1463
|
+
for (let page = 1; ; page++) {
|
|
1464
|
+
const r = await proxy("POST", shop, "/promotion/202309/activities/search", {
|
|
1465
|
+
body: { status, page_size: 50, ...token ? { page_token: token } : {} }
|
|
1466
|
+
});
|
|
1467
|
+
if (!r.ok) return DISCOUNT_TEXT.dupUnclear(title, r.error.message);
|
|
1468
|
+
const d2 = r.data ?? {};
|
|
1469
|
+
for (const a of Array.isArray(d2.activities) ? d2.activities : []) {
|
|
1470
|
+
if (a?.title === title) found.set(String(a.id), { id: String(a.id), status: String(a.status) });
|
|
1471
|
+
}
|
|
1472
|
+
token = d2.next_page_token ? String(d2.next_page_token) : void 0;
|
|
1473
|
+
if (!token) break;
|
|
1474
|
+
if (page === DUP_SEARCH_MAX_PAGES) return DISCOUNT_TEXT.dupUnclear(title, `\u540C\u540D\u81EA\u67E5\u7FFB\u4E86 ${DUP_SEARCH_MAX_PAGES} \u9875\u4ECD\u672A\u7FFB\u5B8C`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if (found.size === 0) return DISCOUNT_TEXT.dupRetitle(title);
|
|
1478
|
+
if (found.size > 1) return DISCOUNT_TEXT.dupMulti(title, [...found.values()].sort((a, b) => Number(a.status !== "NOT_START") - Number(b.status !== "NOT_START")));
|
|
1479
|
+
const [hit] = found.values();
|
|
1480
|
+
const g = await proxy("GET", shop, `/promotion/202309/activities/${hit.id}`, {});
|
|
1481
|
+
if (!g.ok) return DISCOUNT_TEXT.dupUnclear(title, g.error.message);
|
|
1482
|
+
const d = compareReadback(submitted, g.data);
|
|
1483
|
+
if (!d) return DISCOUNT_TEXT.dupUnclear(title, "\u56DE\u8BFB\u7F3A products \u5B57\u6BB5");
|
|
1484
|
+
if (d.missing.length === 0 && d.mismatched.length === 0 && d.total === submitted.length) {
|
|
1485
|
+
return DISCOUNT_TEXT.dupSame(title, hit.id, hit.status);
|
|
1486
|
+
}
|
|
1487
|
+
if (d.productCount === 0) return DISCOUNT_TEXT.dupEmpty(title, hit.id, hit.status, shop);
|
|
1488
|
+
return DISCOUNT_TEXT.dupOwner(title, hit.id, hit.status, problemsText(d) || `\u591A\u51FA ${d.extra.join("\u3001")}`);
|
|
1489
|
+
}
|
|
1184
1490
|
|
|
1185
1491
|
// src/index.ts
|
|
1186
1492
|
var NO_SHOP_COMMANDS = /* @__PURE__ */ new Set(["auth link", "apps add", "apps list", "apps disable", "apps available", "apps set-shop"]);
|
|
@@ -1284,11 +1590,16 @@ if (!match) {
|
|
|
1284
1590
|
usage();
|
|
1285
1591
|
process.exit(1);
|
|
1286
1592
|
}
|
|
1593
|
+
var ctx = { shop: args.shop ?? "", args, actor: args.actor, rest: match.rest };
|
|
1594
|
+
var verdict = match.cmd.validate?.(ctx);
|
|
1595
|
+
if (verdict && !verdict.ok) {
|
|
1596
|
+
out({ ok: false, error: { code: "VALIDATION", message: verdict.message, retryable: false } });
|
|
1597
|
+
process.exit(1);
|
|
1598
|
+
}
|
|
1287
1599
|
if (!args.shop && !NO_SHOP_COMMANDS.has(match.cmd.name)) {
|
|
1288
1600
|
out({ ok: false, error: { code: "VALIDATION", message: "\u7F3A\u5C11 --shop\uFF08\u591A\u5E97\u663E\u5F0F\u8DEF\u7531\uFF09", retryable: false } });
|
|
1289
1601
|
process.exit(1);
|
|
1290
1602
|
}
|
|
1291
|
-
var ctx = { shop: args.shop ?? "", args, actor: args.actor };
|
|
1292
1603
|
if (match.cmd.write && !("apply" in args)) {
|
|
1293
1604
|
if (match.cmd.precheck) {
|
|
1294
1605
|
const pc = await match.cmd.precheck(ctx);
|
|
@@ -1314,7 +1625,7 @@ fillEmptyList(env.data, match.cmd.listKey);
|
|
|
1314
1625
|
if (match.cmd.write && env.ok) {
|
|
1315
1626
|
const result = { ...env, applied: true };
|
|
1316
1627
|
if (env.data?.warnings?.length) {
|
|
1317
|
-
result.warnings_alert = "\u26A0\uFE0F \u6709\
|
|
1628
|
+
result.warnings_alert = "\u26A0\uFE0F \u6709\u8B66\u544A\uFF0C\u89C1 data.warnings";
|
|
1318
1629
|
}
|
|
1319
1630
|
if (match.cmd.readback) {
|
|
1320
1631
|
const rb = await match.cmd.readback(ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optima-chat/tiktok-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "TikTok Shop CLI for Optima agents — 经 tiktok-backend 操作店铺",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"directory": "tiktok-cli"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
|
-
"access": "public"
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org"
|
|
34
35
|
},
|
|
35
36
|
"engines": {
|
|
36
37
|
"node": ">=20"
|