@optima-chat/tiktok-cli 0.11.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +343 -8
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -25,7 +25,7 @@ tiktok <domain> <verb> --shop <shopId> [--flags]
25
25
  | `analytics shop` | 店铺经营数据(`--from --to`,默认近30天) |
26
26
  | `analytics products` | 商品维度数据(GMV 降序) |
27
27
  | `products list` | 商品列表 |
28
- | `promotions list` | 促销活动(`--status`) |
28
+ | `promotions list` | 促销活动。`--status` **省略 = 全部状态**(`ONGOING`/`NOT_START`/`EXPIRED`/`DEACTIVATED`;⚠️ 值写错平台**静默忽略并返回全部**,故 CLI **本地硬拒未知值**;平台将来新增值可用 `--force-status` 越过);`--page-size`(默认 20);`--page-token` 翻页,**末页无 `next_page_token`** |
29
29
  | `finance statements` | 结算单 |
30
30
 
31
31
  ### 建品前置只读辅助(拼商品体用)
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
  }
@@ -217,6 +217,7 @@ ${cmd.summary}
217
217
  var today = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
218
218
  var daysAgo = (n) => new Date(Date.now() - n * 864e5).toISOString().slice(0, 10);
219
219
  var toEpoch = (v) => /^\d+$/.test(v) ? Number(v) : Math.floor(Date.parse(v) / 1e3);
220
+ var PROMOTION_STATUSES = /* @__PURE__ */ new Set(["ONGOING", "NOT_START", "EXPIRED", "DEACTIVATED"]);
220
221
  var COMMANDS = [
221
222
  {
222
223
  name: "orders list",
@@ -598,11 +599,34 @@ var COMMANDS = [
598
599
  },
599
600
  {
600
601
  name: "promotions list",
601
- summary: "\u4FC3\u9500\u6D3B\u52A8\u5217\u8868\uFF08--status ONGOING/NOT_START/EXPIRED\uFF09",
602
+ summary: "\u4FC3\u9500\u6D3B\u52A8\u5217\u8868\uFF08--status \u7701\u7565=\u5168\u90E8\u72B6\u6001\uFF5CONGOING/NOT_START/EXPIRED/DEACTIVATED\uFF0C\u53D6\u503C\u533A\u5206\u5927\u5C0F\u5199\u4E14\u4E0D trim\uFF0C\u672A\u77E5\u503C\u672C\u5730\u62D2\u3001\u5E73\u53F0\u4F1A\u9759\u9ED8\u8FD4\u56DE\u5168\u90E8\uFF1B--force-status \u8D8A\u8FC7\u672C\u5730\u6821\u9A8C\uFF1B--page-size \u9ED8\u8BA420\uFF1B--page-token \u7FFB\u9875\uFF0C\u672B\u9875\u65E0 next_page_token\uFF09",
602
603
  listKey: "activities",
603
- run: ({ shop, args: args2 }) => proxy("POST", shop, "/promotion/202309/activities/search", {
604
- body: { status: args2.status ?? "ONGOING", page_size: Number(args2["page-size"] ?? 20) }
605
- })
604
+ run: ({ shop, args: args2 }) => {
605
+ const st = args2.status ? String(args2.status) : "";
606
+ if (st && !PROMOTION_STATUSES.has(st) && !args2["force-status"]) {
607
+ return Promise.resolve({
608
+ ok: false,
609
+ error: {
610
+ code: "VALIDATION",
611
+ message: `\u672A\u77E5\u7684 --status ${JSON.stringify(st)}\uFF1B\u5DF2\u77E5\u53D6\u503C\uFF1A${[...PROMOTION_STATUSES].join(" / ")}\u3002\u26A0\uFE0F \u5E73\u53F0\u5BF9\u8BA4\u4E0D\u51FA\u7684\u503C**\u4E0D\u62A5\u9519\uFF0C\u800C\u662F\u8FD4\u56DE\u5168\u90E8\u72B6\u6001** \u2014\u2014 \u6240\u4EE5\u8FD9\u91CC\u672C\u5730\u62D2\u6389\uFF0C\u26D4 \u4E0D\u53D1\u8BF7\u6C42\u3002`,
612
+ retryable: false
613
+ }
614
+ });
615
+ }
616
+ return proxy("POST", shop, "/promotion/202309/activities/search", {
617
+ body: {
618
+ // 🔴 省略 --status ⇒ body **不带 status 字段**(⛔ 不是传空串)⇒ 平台回全部状态。
619
+ // 实测(2026-09-19 cn-prod,YOHO Finds):{page_size:100} 无 status → 51 条,
620
+ // 分布 {DEACTIVATED:38, ONGOING:13};显式 DEACTIVATED → 38 条。
621
+ // ⚠️ 原先硬默认 "ONGOING" 使「查全部」无法表达;已知调用点都显式传 --status,不破坏。
622
+ ...args2.status ? { status: String(args2.status) } : {},
623
+ page_size: Number(args2["page-size"] ?? 20),
624
+ // 照同文件 orders 的写法(有值才带)。实测 next_page_token="2"→"4" 可翻页、两页无重叠;
625
+ // **末页该键缺席**(不是空串)⇒ 下游可据此判「读全了」。
626
+ ...args2["page-token"] ? { page_token: String(args2["page-token"]) } : {}
627
+ }
628
+ });
629
+ }
606
630
  },
607
631
  {
608
632
  name: "finance statements",
@@ -742,6 +766,72 @@ var COMMANDS = [
742
766
  },
743
767
  readback: ({ shop, args: args2 }) => proxy("POST", shop, "/promotion/202309/activities/search", { body: { status: "NOT_START", page_size: 20 } })
744
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
+ },
745
835
  {
746
836
  name: "promotions deactivate",
747
837
  summary: "\u7EC8\u6B62/\u505C\u7528\u4FC3\u9500\u6D3B\u52A8\uFF08--id \u5FC5\u586B\uFF09\u3002\u4E0D\u53EF\u9006",
@@ -1157,6 +1247,246 @@ function findCommand(tokens) {
1157
1247
  }
1158
1248
  return null;
1159
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
+ }
1160
1490
 
1161
1491
  // src/index.ts
1162
1492
  var NO_SHOP_COMMANDS = /* @__PURE__ */ new Set(["auth link", "apps add", "apps list", "apps disable", "apps available", "apps set-shop"]);
@@ -1182,7 +1512,7 @@ function parseFlags(tokens) {
1182
1512
  }
1183
1513
  return { args: args2, positional: positional2 };
1184
1514
  }
1185
- var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["apply", "default", "unprocessed", "enable"]);
1515
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["apply", "default", "unprocessed", "enable", "force-status"]);
1186
1516
  var FLAG_TRUE = /* @__PURE__ */ new Set(["true", "1", "yes", "on"]);
1187
1517
  var FLAG_FALSE = /* @__PURE__ */ new Set(["false", "0", "no", "off", ""]);
1188
1518
  function normalizeBooleanFlags(args2) {
@@ -1260,11 +1590,16 @@ if (!match) {
1260
1590
  usage();
1261
1591
  process.exit(1);
1262
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
+ }
1263
1599
  if (!args.shop && !NO_SHOP_COMMANDS.has(match.cmd.name)) {
1264
1600
  out({ ok: false, error: { code: "VALIDATION", message: "\u7F3A\u5C11 --shop\uFF08\u591A\u5E97\u663E\u5F0F\u8DEF\u7531\uFF09", retryable: false } });
1265
1601
  process.exit(1);
1266
1602
  }
1267
- var ctx = { shop: args.shop ?? "", args, actor: args.actor };
1268
1603
  if (match.cmd.write && !("apply" in args)) {
1269
1604
  if (match.cmd.precheck) {
1270
1605
  const pc = await match.cmd.precheck(ctx);
@@ -1290,7 +1625,7 @@ fillEmptyList(env.data, match.cmd.listKey);
1290
1625
  if (match.cmd.write && env.ok) {
1291
1626
  const result = { ...env, applied: true };
1292
1627
  if (env.data?.warnings?.length) {
1293
- result.warnings_alert = "\u26A0\uFE0F \u6709\u5B57\u6BB5\u88AB\u9759\u9ED8\u5904\u7406\uFF0C\u89C1 warnings";
1628
+ result.warnings_alert = "\u26A0\uFE0F \u6709\u8B66\u544A\uFF0C\u89C1 data.warnings";
1294
1629
  }
1295
1630
  if (match.cmd.readback) {
1296
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.11.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"