@optima-chat/tiktok-cli 0.2.0 → 0.3.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/README.md +23 -1
- package/dist/index.js +187 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ tiktok <domain> <verb> --shop <shopId> [--flags]
|
|
|
19
19
|
| 命令 | 说明 |
|
|
20
20
|
|---|---|
|
|
21
21
|
| `auth link` | 生成店铺绑定授权链接(无需 --shop) |
|
|
22
|
-
| `shops
|
|
22
|
+
| `shops` | 查当前登录用户已授权店铺 |
|
|
23
23
|
| `orders list` | 订单列表(`--status --page-size --since`) |
|
|
24
24
|
| `orders get --id <id>` | 订单详情 |
|
|
25
25
|
| `analytics shop` | 店铺经营数据(`--from --to`,默认近30天) |
|
|
@@ -28,6 +28,22 @@ tiktok <domain> <verb> --shop <shopId> [--flags]
|
|
|
28
28
|
| `promotions list` | 促销活动(`--status`) |
|
|
29
29
|
| `finance statements` | 结算单 |
|
|
30
30
|
|
|
31
|
+
### 建品前置只读辅助(拼商品体用)
|
|
32
|
+
|
|
33
|
+
| 命令 | 说明 |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `products prerequisites` | 建品前置检查(店铺就绪状态) |
|
|
36
|
+
| `categories recommend --title [--desc]` | 类目推荐 |
|
|
37
|
+
| `categories tree [--version v2]` | 类目树 |
|
|
38
|
+
| `categories attributes --category <id>` | 类目属性(看 is_requried 必填项) |
|
|
39
|
+
| `categories rules --category <id>` | 类目规则(尺码表/认证/包装是否强制) |
|
|
40
|
+
| `brands list [--category] [--name]` | 品牌列表 |
|
|
41
|
+
| `brands recommend --title <t>` | 品牌推荐 |
|
|
42
|
+
| `warehouses list` | 仓库列表(跨境认 sub_type CB_*) |
|
|
43
|
+
| `shipping-templates list` | 运费模板列表 |
|
|
44
|
+
| `delivery-options --warehouse <id>` | 仓库交付方式 |
|
|
45
|
+
| `images upload --file <path> --use-case MAIN_IMAGE [--out <f>]` | 上传本地图 → 返回 TikTok CDN `uri`(multipart,供拼 `main_images` 用) |
|
|
46
|
+
|
|
31
47
|
## 写操作(Phase 2,安全护栏)
|
|
32
48
|
|
|
33
49
|
写命令三道阀,把 A 轨「填完回读」纪律做进 CLI:
|
|
@@ -49,9 +65,15 @@ tiktok products set-price --shop <id> --product <pid> --sku <sid> --price 29.90
|
|
|
49
65
|
| `products set-inventory` | 改 SKU 库存(`--product --sku --warehouse --quantity`) | ✓ 49→55→49 可逆 |
|
|
50
66
|
| `promotions create-flashsale` | 建秒杀(`--title --product --sku --price [--limit --per-user --hours]`)。两步:建壳+加品 | ✓ 真实建成 |
|
|
51
67
|
| `promotions deactivate` | 终止促销活动(`--id`)。不可逆 | ✓ |
|
|
68
|
+
| `products create --body-file <json>` | **生产级建品**(完整商品体 JSON)。dry-run 内嵌 `listing_check` 预检 → `--apply` 建品 → 回读高亮 warnings | ✓ 真实上架 V.V.Love(ACTIVATE) |
|
|
69
|
+
| `products add-variant --product <id> --body-file <json>` | 加规格 SKU(读现有 skus → merge → partial_edit;新 SKU 自动继承 sku_dimensions/sku_weight) | ✓ 真加变体成功 |
|
|
70
|
+
| `products replace-images --product <id> --body-file <json>` | 整组换主图(main_images 整组替换,非追加) | — |
|
|
71
|
+
| `products partial-edit --product <id> --body-file <json>` | 改商品字段(嵌套字段整组覆盖,未列子项会被清空) | — |
|
|
52
72
|
|
|
53
73
|
均在跨境测试店真实验证(2026-09-02):`ok/applied` → 回读确认 → 审计落库。
|
|
54
74
|
|
|
75
|
+
**建品字段坑**(实测坐实,见 [`../docs/建品字段权威规格.md`](../docs/建品字段权威规格.md)):MY 跨境店价格用 `sale_price` 非 `amount`;`package_dimensions` 类目必填;仓库认 `sub_type=CB_*`;**品牌合规**——标题/主图的品牌须与 `brand_id` 一致,否则审核报「品牌不一致违规」(用第三方品牌名却不申报会被拒);`add-variant` 加的新 SKU 必须带 `sku_dimensions`/`sku_weight`(建品时平台从 package 自动回填,加变体无此回填,CLI 已自动继承)。图片经 `images upload` 传本地文件(backend multipart 路由)。
|
|
76
|
+
|
|
55
77
|
**秒杀字段坑**(OAS 未标、实测撞出):商品级 `quantity_limit` 在 VARIATION 模式必须 `-1`(限量由 SKU 级控制);秒杀价须**低于该 SKU 近 30 天最低成交价**且**不超折扣上限**。这些复杂度被 CLI 吸收,agent 一次成型。
|
|
56
78
|
|
|
57
79
|
## 错误分类(agent 靠此决策)
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/commands.ts
|
|
4
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
|
+
|
|
3
6
|
// src/client.ts
|
|
4
7
|
import { readFileSync } from "fs";
|
|
5
8
|
import { homedir } from "os";
|
|
9
|
+
import { basename } from "path";
|
|
6
10
|
import { join } from "path";
|
|
7
11
|
|
|
8
12
|
// src/backendUrl.ts
|
|
@@ -81,8 +85,37 @@ async function createAuthLink() {
|
|
|
81
85
|
return { ok: false, error: { code: "BACKEND_UNREACHABLE", message: e.message, retryable: true } };
|
|
82
86
|
}
|
|
83
87
|
}
|
|
88
|
+
async function uploadImage(shop, filePath, useCase) {
|
|
89
|
+
try {
|
|
90
|
+
const buf = readFileSync(filePath);
|
|
91
|
+
const form = new FormData();
|
|
92
|
+
form.append("data", new Blob([buf]), basename(filePath));
|
|
93
|
+
form.append("use_case", useCase);
|
|
94
|
+
const r = await fetch(`${BACKEND}/proxy-upload/${shop}/image`, { method: "POST", headers: authHeaders(), body: form });
|
|
95
|
+
const body = await r.json().catch(() => ({}));
|
|
96
|
+
if (body?.code === 0) return { ok: true, data: { uri: body.data?.uri, url: body.data?.url, use_case: useCase }, meta: { shop } };
|
|
97
|
+
return { ok: false, error: { code: r.status === 401 ? "AUTH_EXPIRED" : "TIKTOK_ERROR", message: body?.message ?? body?.error ?? `HTTP ${r.status}`, retryable: false }, meta: { shop } };
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return { ok: false, error: { code: "BACKEND_UNREACHABLE", message: e.message, retryable: true } };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
84
102
|
|
|
85
103
|
// src/commands.ts
|
|
104
|
+
function readBodyFile(path) {
|
|
105
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
106
|
+
}
|
|
107
|
+
function mergeSkus(existing, add) {
|
|
108
|
+
const template = existing?.[0];
|
|
109
|
+
const filledAdd = add.map((sku) => {
|
|
110
|
+
if (!template) return sku;
|
|
111
|
+
return {
|
|
112
|
+
...sku,
|
|
113
|
+
sku_dimensions: sku.sku_dimensions ?? template.sku_dimensions,
|
|
114
|
+
sku_weight: sku.sku_weight ?? template.sku_weight
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
return [...existing, ...filledAdd];
|
|
118
|
+
}
|
|
86
119
|
var today = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
87
120
|
var daysAgo = (n) => new Date(Date.now() - n * 864e5).toISOString().slice(0, 10);
|
|
88
121
|
var COMMANDS = [
|
|
@@ -152,6 +185,78 @@ var COMMANDS = [
|
|
|
152
185
|
}
|
|
153
186
|
})
|
|
154
187
|
},
|
|
188
|
+
// ===== 建品前置只读辅助命令 =====
|
|
189
|
+
{
|
|
190
|
+
name: "products prerequisites",
|
|
191
|
+
summary: "\u5EFA\u54C1\u524D\u7F6E\u68C0\u67E5",
|
|
192
|
+
run: ({ shop }) => proxy("GET", shop, "/product/202312/prerequisites", {})
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "categories recommend",
|
|
196
|
+
summary: "\u7C7B\u76EE\u63A8\u8350\uFF08--title [--desc]\uFF09",
|
|
197
|
+
run: ({ shop, args: args2 }) => proxy("POST", shop, "/product/202309/categories/recommend", {
|
|
198
|
+
body: {
|
|
199
|
+
product_title: args2.title,
|
|
200
|
+
...args2.desc ? { description: args2.desc } : {}
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "categories tree",
|
|
206
|
+
summary: "\u7C7B\u76EE\u6811\uFF08--version v2\uFF09",
|
|
207
|
+
run: ({ shop, args: args2 }) => proxy("GET", shop, "/product/202309/categories", {
|
|
208
|
+
query: { category_version: args2.version ?? "v2" }
|
|
209
|
+
})
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "categories attributes",
|
|
213
|
+
summary: "\u7C7B\u76EE\u5C5E\u6027\uFF08--category\uFF09",
|
|
214
|
+
run: ({ shop, args: args2 }) => proxy("GET", shop, `/product/202309/categories/${args2.category}/attributes`, {
|
|
215
|
+
query: { category_version: args2.version ?? "v2" }
|
|
216
|
+
})
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: "categories rules",
|
|
220
|
+
summary: "\u7C7B\u76EE\u89C4\u5219\uFF08--category\uFF09",
|
|
221
|
+
run: ({ shop, args: args2 }) => proxy("GET", shop, `/product/202309/categories/${args2.category}/rules`, {
|
|
222
|
+
query: { category_version: args2.version ?? "v2" }
|
|
223
|
+
})
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "brands list",
|
|
227
|
+
summary: "\u54C1\u724C\u5217\u8868\uFF08[--category] [--name]\uFF09",
|
|
228
|
+
run: ({ shop, args: args2 }) => proxy("GET", shop, "/product/202309/brands", {
|
|
229
|
+
query: {
|
|
230
|
+
page_size: args2["page-size"] ?? "50",
|
|
231
|
+
...args2.category ? { category_id: args2.category } : {},
|
|
232
|
+
...args2.name ? { brand_name: args2.name } : {}
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "brands recommend",
|
|
238
|
+
summary: "\u54C1\u724C\u63A8\u8350\uFF08--title\uFF09",
|
|
239
|
+
run: ({ shop, args: args2 }) => proxy("POST", shop, "/product/202309/brands/recommend", {
|
|
240
|
+
body: { product_title: args2.title }
|
|
241
|
+
})
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: "warehouses list",
|
|
245
|
+
summary: "\u4ED3\u5E93\u5217\u8868\uFF08\u8DE8\u5883\u8BA4 sub_type CB_*\uFF09",
|
|
246
|
+
run: ({ shop }) => proxy("GET", shop, "/logistics/202309/warehouses", {})
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "shipping-templates list",
|
|
250
|
+
summary: "\u8FD0\u8D39\u6A21\u677F\u5217\u8868",
|
|
251
|
+
run: ({ shop }) => proxy("GET", shop, "/logistics/202510/seller_templates", {})
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "delivery-options",
|
|
255
|
+
summary: "\u4ED3\u5E93\u4EA4\u4ED8\u65B9\u5F0F\uFF08--warehouse\uFF09",
|
|
256
|
+
run: ({ shop, args: args2 }) => proxy("GET", shop, `/logistics/202309/warehouses/${args2.warehouse}/delivery_options`, {
|
|
257
|
+
query: { scope: "PRODUCT" }
|
|
258
|
+
})
|
|
259
|
+
},
|
|
155
260
|
// ===== 写操作(Phase 2):默认 --dry-run,--apply 才真发,成功后回读 =====
|
|
156
261
|
{
|
|
157
262
|
name: "products set-price",
|
|
@@ -209,6 +314,49 @@ var COMMANDS = [
|
|
|
209
314
|
}],
|
|
210
315
|
run: ({ shop, args: args2 }) => proxy("POST", shop, `/product/202309/products/${args2.product}/inventory/update`, { body: inventoryBody(args2) }),
|
|
211
316
|
readback: ({ shop, args: args2 }) => proxy("GET", shop, `/product/202309/products/${args2.product}`, {})
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: "products create",
|
|
320
|
+
summary: "\u5EFA\u54C1\uFF08--body-file [--apply]\uFF09\u3002dry-run \u8DD1 listing_check \u9884\u68C0\uFF1B\u5199\u64CD\u4F5C",
|
|
321
|
+
write: true,
|
|
322
|
+
preview: () => [{ step: "listing_check \u9884\u68C0 \u2192 \u5EFA\u54C1", method: "POST", path: "/product/202309/products/listing_check", body: void 0 }],
|
|
323
|
+
precheck: ({ shop, args: args2 }) => proxy("POST", shop, "/product/202309/products/listing_check", { body: readBodyFile(args2["body-file"]) }),
|
|
324
|
+
run: async ({ shop, args: args2 }) => {
|
|
325
|
+
const body = readBodyFile(args2["body-file"]);
|
|
326
|
+
return proxy("POST", shop, "/product/202309/products", { body });
|
|
327
|
+
},
|
|
328
|
+
readback: async ({ shop, args: args2 }) => {
|
|
329
|
+
return proxy("POST", shop, "/product/202309/products/search", { query: { page_size: "5" }, body: {} });
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: "products add-variant",
|
|
334
|
+
summary: "\u7ED9\u5546\u54C1\u52A0\u89C4\u683C SKU\uFF08--product --body-file skus-delta.json [--apply]\uFF09",
|
|
335
|
+
write: true,
|
|
336
|
+
preview: () => [{ step: "\u8BFB\u73B0\u6709 skus \u2192 append \u2192 partial_edit", method: "POST", path: "/product/202309/products/{id}/partial_edit" }],
|
|
337
|
+
run: async ({ shop, args: args2 }) => {
|
|
338
|
+
const cur = await proxy("GET", shop, `/product/202309/products/${args2.product}`, {});
|
|
339
|
+
if (!cur.ok) return cur;
|
|
340
|
+
const merged = mergeSkus(cur.data?.skus ?? [], readBodyFile(args2["body-file"]).skus ?? []);
|
|
341
|
+
return proxy("POST", shop, `/product/202309/products/${args2.product}/partial_edit`, { body: { skus: merged } });
|
|
342
|
+
},
|
|
343
|
+
readback: ({ shop, args: args2 }) => proxy("GET", shop, `/product/202309/products/${args2.product}`, {})
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "products replace-images",
|
|
347
|
+
summary: "\u6574\u7EC4\u6362\u4E3B\u56FE\uFF08--product --body-file images.json [--apply]\uFF09",
|
|
348
|
+
write: true,
|
|
349
|
+
preview: () => [{ step: "main_images \u6574\u7EC4\u66FF\u6362 \u2192 partial_edit", method: "POST", path: "/product/202309/products/{id}/partial_edit" }],
|
|
350
|
+
run: ({ shop, args: args2 }) => proxy("POST", shop, `/product/202309/products/${args2.product}/partial_edit`, { body: { main_images: readBodyFile(args2["body-file"]).main_images } }),
|
|
351
|
+
readback: ({ shop, args: args2 }) => proxy("GET", shop, `/product/202309/products/${args2.product}`, {})
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: "products partial-edit",
|
|
355
|
+
summary: "\u6539\u5546\u54C1\u5B57\u6BB5\uFF08--product --body-file patch.json [--apply]\uFF09\u3002\u5D4C\u5957\u5B57\u6BB5\u6574\u7EC4\u8986\u76D6",
|
|
356
|
+
write: true,
|
|
357
|
+
preview: ({ args: args2 }) => [{ step: "partial_edit\uFF08\u5D4C\u5957\u5B57\u6BB5\u672A\u5217\u5B50\u9879\u4F1A\u88AB\u6E05\u7A7A\uFF09", method: "POST", path: `/product/202309/products/${args2.product}/partial_edit`, body: readBodyFile(args2["body-file"]) }],
|
|
358
|
+
run: ({ shop, args: args2 }) => proxy("POST", shop, `/product/202309/products/${args2.product}/partial_edit`, { body: readBodyFile(args2["body-file"]) }),
|
|
359
|
+
readback: ({ shop, args: args2 }) => proxy("GET", shop, `/product/202309/products/${args2.product}`, {})
|
|
212
360
|
}
|
|
213
361
|
];
|
|
214
362
|
function inventoryBody(args2) {
|
|
@@ -292,7 +440,8 @@ function usage() {
|
|
|
292
440
|
for (const c of COMMANDS) lines.push(` ${c.name.padEnd(22)} ${c.summary}`);
|
|
293
441
|
lines.push(` ${"shops".padEnd(22)} \u67E5\u5F53\u524D\u767B\u5F55\u7528\u6237\u5DF2\u6388\u6743\u5E97\u94FA`);
|
|
294
442
|
lines.push(` ${"auth link".padEnd(22)} \u751F\u6210\u5E97\u94FA\u7ED1\u5B9A\u6388\u6743\u94FE\u63A5\uFF08\u53D1\u7ED9\u5546\u5BB6\u70B9\u51FB\uFF09`);
|
|
295
|
-
lines.push("
|
|
443
|
+
lines.push(` ${"images upload".padEnd(22)} \u4E0A\u4F20\u672C\u5730\u56FE\u7247\uFF08--file --use-case [--out]\uFF09`);
|
|
444
|
+
lines.push("", "\u901A\u7528\uFF1A--shop \u5FC5\u586B\uFF08\u9664 shops / auth link\uFF09\uFF1B\u9ED8\u8BA4 JSON \u8F93\u51FA\uFF1BTIKTOK_BACKEND_URL \u663E\u5F0F\u6307\u5B9A backend\uFF08\u672A\u8BBE\u5219\u6309 OPTIMA_ENV \u6620\u5C04 cn-stage/cn-prod\uFF0C\u5426\u5219 localhost\uFF09\u3002");
|
|
296
445
|
process.stderr.write(lines.join("\n") + "\n");
|
|
297
446
|
}
|
|
298
447
|
var argv = process.argv.slice(2);
|
|
@@ -309,6 +458,23 @@ if (positional[0] === "auth" && positional[1] === "link") {
|
|
|
309
458
|
out(await createAuthLink());
|
|
310
459
|
process.exit(0);
|
|
311
460
|
}
|
|
461
|
+
if (positional[0] === "images" && positional[1] === "upload") {
|
|
462
|
+
if (!args.file) {
|
|
463
|
+
out({ ok: false, error: { code: "VALIDATION", message: "\u7F3A\u5C11 --file", retryable: false } });
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
if (!args.shop) {
|
|
467
|
+
out({ ok: false, error: { code: "VALIDATION", message: "\u7F3A\u5C11 --shop", retryable: false } });
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
const r = await uploadImage(args.shop, args.file, args["use-case"] ?? "MAIN_IMAGE");
|
|
471
|
+
if (r.ok && args.out) {
|
|
472
|
+
const { appendFileSync } = await import("fs");
|
|
473
|
+
appendFileSync(args.out, JSON.stringify(r.data) + "\n");
|
|
474
|
+
}
|
|
475
|
+
out(r);
|
|
476
|
+
process.exit(0);
|
|
477
|
+
}
|
|
312
478
|
var match = findCommand(positional);
|
|
313
479
|
if (!match) {
|
|
314
480
|
out({ ok: false, error: { code: "VALIDATION", message: `\u672A\u77E5\u547D\u4EE4: ${positional.join(" ")}`, retryable: false } });
|
|
@@ -321,6 +487,16 @@ if (!args.shop) {
|
|
|
321
487
|
}
|
|
322
488
|
var ctx = { shop: args.shop, args, actor: args.actor };
|
|
323
489
|
if (match.cmd.write && !("apply" in args)) {
|
|
490
|
+
if (match.cmd.precheck) {
|
|
491
|
+
const pc = await match.cmd.precheck(ctx);
|
|
492
|
+
out({
|
|
493
|
+
ok: true,
|
|
494
|
+
dry_run: true,
|
|
495
|
+
precheck: pc.ok ? pc.data : { error: pc.error },
|
|
496
|
+
message: "listing_check \u9884\u68C0\u7ED3\u679C\uFF0C\u786E\u8BA4\u540E\u52A0 --apply \u6267\u884C\u3002"
|
|
497
|
+
});
|
|
498
|
+
process.exit(0);
|
|
499
|
+
}
|
|
324
500
|
const plan = match.cmd.preview?.(ctx);
|
|
325
501
|
out({
|
|
326
502
|
ok: true,
|
|
@@ -331,9 +507,16 @@ if (match.cmd.write && !("apply" in args)) {
|
|
|
331
507
|
process.exit(0);
|
|
332
508
|
}
|
|
333
509
|
var env = await match.cmd.run(ctx);
|
|
334
|
-
if (match.cmd.write && env.ok
|
|
335
|
-
const
|
|
336
|
-
|
|
510
|
+
if (match.cmd.write && env.ok) {
|
|
511
|
+
const result = { ...env, applied: true };
|
|
512
|
+
if (env.data?.warnings?.length) {
|
|
513
|
+
result.warnings_alert = "\u26A0\uFE0F \u6709\u5B57\u6BB5\u88AB\u9759\u9ED8\u5904\u7406\uFF0C\u89C1 warnings";
|
|
514
|
+
}
|
|
515
|
+
if (match.cmd.readback) {
|
|
516
|
+
const rb = await match.cmd.readback(ctx);
|
|
517
|
+
result.readback = rb.ok ? rb.data : { error: rb.error };
|
|
518
|
+
}
|
|
519
|
+
out(result);
|
|
337
520
|
} else {
|
|
338
521
|
out(env);
|
|
339
522
|
}
|