@optima-chat/tiktok-cli 0.1.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 +71 -0
- package/dist/index.js +312 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# tiktok-cli
|
|
2
|
+
|
|
3
|
+
Optima agent 用来操作 TikTok 店铺的 CLI。**薄客户端**:不直连 TikTok、不持凭证,一切经 [tiktok-backend](../tiktok-backend) 的 `/proxy`(backend 解析 shop → 取 token → 签名 → 转发)。
|
|
4
|
+
|
|
5
|
+
这是「把 xuhao repo 的浏览器 skill 改成 CLI」的落地——agent 一行命令拉到真实店铺数据,替代紫鸟 + Playwright。
|
|
6
|
+
|
|
7
|
+
## 用法
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
tiktok <domain> <verb> --shop <shopId> [--flags]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- `--shop` **必填**(多店显式路由,对应 A 轨「多店必须显式选店」教训)
|
|
14
|
+
- 默认 **JSON 输出**,统一信封 `{ ok, data?, error?, meta? }`
|
|
15
|
+
- `TIKTOK_BACKEND_URL` 指向 backend(默认 `http://127.0.0.1:3000`)
|
|
16
|
+
|
|
17
|
+
## 命令(v1 只读)
|
|
18
|
+
|
|
19
|
+
| 命令 | 说明 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `shops --merchant <id>` | 查商家已授权店铺 |
|
|
22
|
+
| `orders list` | 订单列表(`--status --page-size --since`) |
|
|
23
|
+
| `orders get --id <id>` | 订单详情 |
|
|
24
|
+
| `analytics shop` | 店铺经营数据(`--from --to`,默认近30天) |
|
|
25
|
+
| `analytics products` | 商品维度数据(GMV 降序) |
|
|
26
|
+
| `products list` | 商品列表 |
|
|
27
|
+
| `promotions list` | 促销活动(`--status`) |
|
|
28
|
+
| `finance statements` | 结算单 |
|
|
29
|
+
|
|
30
|
+
## 写操作(Phase 2,安全护栏)
|
|
31
|
+
|
|
32
|
+
写命令三道阀,把 A 轨「填完回读」纪律做进 CLI:
|
|
33
|
+
|
|
34
|
+
1. **默认 dry-run** —— 不加 `--apply` 只打印将提交的 payload,不碰真实店铺
|
|
35
|
+
2. `--apply` 才真发
|
|
36
|
+
3. **成功后自动回读** —— 写完 GET 回资源,输出 `readback` 供 agent/人核对是否生效
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# 预览(安全)
|
|
40
|
+
tiktok products set-price --shop <id> --product <pid> --sku <sid> --price 29.90
|
|
41
|
+
# 确认后执行
|
|
42
|
+
tiktok products set-price --shop <id> --product <pid> --sku <sid> --price 29.90 --apply
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| 命令 | 说明 | 验证 |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `products set-price` | 改 SKU 售价(`--product --sku --price [--currency]`) | ✓ 改价+回读 |
|
|
48
|
+
| `products set-inventory` | 改 SKU 库存(`--product --sku --warehouse --quantity`) | ✓ 49→55→49 可逆 |
|
|
49
|
+
| `promotions create-flashsale` | 建秒杀(`--title --product --sku --price [--limit --per-user --hours]`)。两步:建壳+加品 | ✓ 真实建成 |
|
|
50
|
+
| `promotions deactivate` | 终止促销活动(`--id`)。不可逆 | ✓ |
|
|
51
|
+
|
|
52
|
+
均在跨境测试店真实验证(2026-09-02):`ok/applied` → 回读确认 → 审计落库。
|
|
53
|
+
|
|
54
|
+
**秒杀字段坑**(OAS 未标、实测撞出):商品级 `quantity_limit` 在 VARIATION 模式必须 `-1`(限量由 SKU 级控制);秒杀价须**低于该 SKU 近 30 天最低成交价**且**不超折扣上限**。这些复杂度被 CLI 吸收,agent 一次成型。
|
|
55
|
+
|
|
56
|
+
## 错误分类(agent 靠此决策)
|
|
57
|
+
|
|
58
|
+
`error.code` + `retryable` 让 agent 判断重试还是停止:
|
|
59
|
+
|
|
60
|
+
| code | 含义 | retryable |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `RATE_LIMITED` | 限流 | ✅(带 retry_after_s) |
|
|
63
|
+
| `BACKEND_UNREACHABLE` | 连不上 backend | ✅ |
|
|
64
|
+
| `AUTH_EXPIRED` | token/授权失效 | ❌ 需重授权 |
|
|
65
|
+
| `SHOP_NOT_AUTHORIZED` | 无此店授权 | ❌ |
|
|
66
|
+
| `VALIDATION` | 参数/业务错误 | ❌ |
|
|
67
|
+
| `TIKTOK_ERROR` | TikTok 其他错误(透传码) | ❌ |
|
|
68
|
+
|
|
69
|
+
## 命令 → 端点映射
|
|
70
|
+
|
|
71
|
+
命令定义见 `src/commands.ts`,每条对应一个 TikTok 端点,逐条对齐 xuhao repo 的浏览器 skill(见 [接口草案](../docs/tiktok-backend-接口草案.md))。加命令只动这张表。
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/client.ts
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
var BACKEND = process.env.TIKTOK_BACKEND_URL ?? "http://127.0.0.1:3000";
|
|
8
|
+
function authHeaders() {
|
|
9
|
+
const explicit = process.env.OPTIMA_TOKEN;
|
|
10
|
+
if (explicit) return { Authorization: `Bearer ${explicit}` };
|
|
11
|
+
try {
|
|
12
|
+
const t = JSON.parse(readFileSync(join(homedir(), ".optima", "token.json"), "utf8"));
|
|
13
|
+
if (t.access_token) return { Authorization: `Bearer ${t.access_token}` };
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
function classify(status, body, shop) {
|
|
19
|
+
const meta = { shop, tiktok_request_id: body?.request_id };
|
|
20
|
+
if (status === 0 || body?.code === 0) {
|
|
21
|
+
return { ok: true, data: body.data, meta };
|
|
22
|
+
}
|
|
23
|
+
if (status === 404) return { ok: false, error: { code: "SHOP_NOT_AUTHORIZED", message: body?.error ?? "\u5E97\u94FA\u672A\u6388\u6743", retryable: false }, meta };
|
|
24
|
+
if (status === 403) return { ok: false, error: { code: "VALIDATION", message: body?.error ?? "\u8DEF\u5F84\u4E0D\u5141\u8BB8", retryable: false }, meta };
|
|
25
|
+
const tc = body?.code;
|
|
26
|
+
const msg = body?.message ?? body?.error ?? `HTTP ${status}`;
|
|
27
|
+
if (tc === 36009002 || status === 429) return { ok: false, error: { code: "RATE_LIMITED", message: msg, retryable: true, retry_after_s: 30 }, meta };
|
|
28
|
+
if (tc === 105002 || tc === 105005 || status === 401) return { ok: false, error: { code: "AUTH_EXPIRED", message: msg, retryable: false }, meta };
|
|
29
|
+
if (typeof tc === "number" && tc >= 1e5) return { ok: false, error: { code: "TIKTOK_ERROR", message: `[${tc}] ${msg}`, retryable: false }, meta };
|
|
30
|
+
return { ok: false, error: { code: "VALIDATION", message: msg, retryable: false }, meta };
|
|
31
|
+
}
|
|
32
|
+
async function proxy(method, shop, path, opts = {}) {
|
|
33
|
+
const qs = opts.query ? "?" + new URLSearchParams(opts.query) : "";
|
|
34
|
+
const url = `${BACKEND}/proxy/${shop}${path}${qs}`;
|
|
35
|
+
try {
|
|
36
|
+
const r = await fetch(url, {
|
|
37
|
+
method,
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
"x-optima-actor": opts.actor ?? process.env.USER ?? "cli",
|
|
41
|
+
...authHeaders()
|
|
42
|
+
},
|
|
43
|
+
body: method === "POST" || method === "PUT" ? JSON.stringify(opts.body ?? {}) : void 0
|
|
44
|
+
});
|
|
45
|
+
const body = await r.json().catch(() => ({}));
|
|
46
|
+
return classify(r.status, body, shop);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return { ok: false, error: { code: "BACKEND_UNREACHABLE", message: `\u8FDE\u4E0D\u4E0A backend ${BACKEND}: ${e.message}`, retryable: true } };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function listShops() {
|
|
52
|
+
try {
|
|
53
|
+
const r = await fetch(`${BACKEND}/auth/shops`, { headers: authHeaders() });
|
|
54
|
+
if (r.status === 401) return { ok: false, error: { code: "AUTH_EXPIRED", message: "\u672A\u767B\u5F55\u6216 token \u5931\u6548", retryable: false } };
|
|
55
|
+
const body = await r.json();
|
|
56
|
+
return { ok: true, data: body.shops };
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return { ok: false, error: { code: "BACKEND_UNREACHABLE", message: e.message, retryable: true } };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/commands.ts
|
|
63
|
+
var today = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
64
|
+
var daysAgo = (n) => new Date(Date.now() - n * 864e5).toISOString().slice(0, 10);
|
|
65
|
+
var COMMANDS = [
|
|
66
|
+
{
|
|
67
|
+
name: "orders list",
|
|
68
|
+
summary: "\u8BA2\u5355\u5217\u8868\uFF08\u9ED8\u8BA4\u8FD130\u5929\uFF0C\u53EF --status --page-size\uFF09",
|
|
69
|
+
run: ({ shop, args: args2 }) => proxy("POST", shop, "/order/202309/orders/search", {
|
|
70
|
+
query: { page_size: args2["page-size"] ?? "20" },
|
|
71
|
+
body: {
|
|
72
|
+
...args2.status ? { order_status: args2.status } : {},
|
|
73
|
+
create_time_ge: args2.since ? Number(args2.since) : Math.floor(Date.parse(daysAgo(30)) / 1e3)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "orders get",
|
|
79
|
+
summary: "\u8BA2\u5355\u8BE6\u60C5\uFF08--id \u5FC5\u586B\uFF09",
|
|
80
|
+
run: ({ shop, args: args2 }) => proxy("GET", shop, "/order/202309/orders/detail", { query: { ids: args2.id } })
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "analytics shop",
|
|
84
|
+
summary: "\u5E97\u94FA\u7ECF\u8425\u6570\u636E\uFF08--from --to\uFF0C\u9ED8\u8BA4\u8FD130\u5929\uFF09",
|
|
85
|
+
run: ({ shop, args: args2 }) => proxy("GET", shop, "/analytics/202509/shop/performance", {
|
|
86
|
+
query: {
|
|
87
|
+
start_date_ge: args2.from ?? daysAgo(30),
|
|
88
|
+
end_date_lt: args2.to ?? today(),
|
|
89
|
+
granularity: args2.granularity ?? "ALL"
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "analytics products",
|
|
95
|
+
summary: "\u5546\u54C1\u7EF4\u5EA6\u7ECF\u8425\u6570\u636E\uFF08\u6309 GMV \u964D\u5E8F\uFF09",
|
|
96
|
+
run: ({ shop, args: args2 }) => proxy("GET", shop, "/analytics/202509/shop_products/performance", {
|
|
97
|
+
query: {
|
|
98
|
+
start_date_ge: args2.from ?? daysAgo(30),
|
|
99
|
+
end_date_lt: args2.to ?? today(),
|
|
100
|
+
sort_field: "gmv",
|
|
101
|
+
sort_order: "DESC",
|
|
102
|
+
page_size: args2["page-size"] ?? "20"
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "products list",
|
|
108
|
+
summary: "\u5546\u54C1\u5217\u8868",
|
|
109
|
+
run: ({ shop, args: args2 }) => proxy("POST", shop, "/product/202309/products/search", {
|
|
110
|
+
query: { page_size: args2["page-size"] ?? "20" },
|
|
111
|
+
body: {}
|
|
112
|
+
})
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "promotions list",
|
|
116
|
+
summary: "\u4FC3\u9500\u6D3B\u52A8\u5217\u8868\uFF08--status ONGOING/NOT_START/EXPIRED\uFF09",
|
|
117
|
+
run: ({ shop, args: args2 }) => proxy("POST", shop, "/promotion/202309/activities/search", {
|
|
118
|
+
body: { status: args2.status ?? "ONGOING", page_size: Number(args2["page-size"] ?? 20) }
|
|
119
|
+
})
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "finance statements",
|
|
123
|
+
summary: "\u7ED3\u7B97\u5355\u5217\u8868",
|
|
124
|
+
run: ({ shop, args: args2 }) => proxy("GET", shop, "/finance/202309/statements", {
|
|
125
|
+
query: {
|
|
126
|
+
page_size: args2["page-size"] ?? "20",
|
|
127
|
+
sort_field: args2["sort-field"] ?? "statement_time",
|
|
128
|
+
sort_order: args2["sort-order"] ?? "DESC"
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
},
|
|
132
|
+
// ===== 写操作(Phase 2):默认 --dry-run,--apply 才真发,成功后回读 =====
|
|
133
|
+
{
|
|
134
|
+
name: "products set-price",
|
|
135
|
+
summary: "\u6539 SKU \u552E\u4EF7\uFF08--product --sku --price [--currency MYR]\uFF09\u3002\u5199\u64CD\u4F5C,\u9ED8\u8BA4 dry-run",
|
|
136
|
+
write: true,
|
|
137
|
+
preview: ({ args: args2 }) => [{
|
|
138
|
+
step: "\u6539\u4EF7",
|
|
139
|
+
method: "POST",
|
|
140
|
+
path: `/product/202309/products/${args2.product}/prices/update`,
|
|
141
|
+
body: priceBody(args2)
|
|
142
|
+
}],
|
|
143
|
+
run: ({ shop, args: args2 }) => proxy("POST", shop, `/product/202309/products/${args2.product}/prices/update`, { body: priceBody(args2) }),
|
|
144
|
+
readback: ({ shop, args: args2 }) => proxy("GET", shop, `/product/202309/products/${args2.product}`, {})
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "promotions create-flashsale",
|
|
148
|
+
summary: "\u5EFA\u79D2\u6740\u6D3B\u52A8\uFF08--title --product --sku --price [--limit] [--hours 48]\uFF09\u3002\u4E24\u6B65:\u5EFA\u58F3+\u52A0\u54C1",
|
|
149
|
+
write: true,
|
|
150
|
+
preview: ({ args: args2 }) => {
|
|
151
|
+
const { begin, end } = flashWindow(args2);
|
|
152
|
+
return [
|
|
153
|
+
{ step: "1/2 \u5EFA\u6D3B\u52A8\u58F3", method: "POST", path: "/promotion/202309/activities", body: flashActivityBody(args2, begin, end) },
|
|
154
|
+
{ step: "2/2 \u52A0\u5546\u54C1+\u79D2\u6740\u4EF7", method: "PUT", path: "/promotion/202309/activities/{activity_id}/products", body: flashProductsBody(args2) }
|
|
155
|
+
];
|
|
156
|
+
},
|
|
157
|
+
run: async ({ shop, args: args2 }) => {
|
|
158
|
+
const { begin, end } = flashWindow(args2);
|
|
159
|
+
const created = await proxy("POST", shop, "/promotion/202309/activities", { body: flashActivityBody(args2, begin, end) });
|
|
160
|
+
if (!created.ok) return created;
|
|
161
|
+
const activityId = created.data?.activity_id ?? created.data?.activity?.id;
|
|
162
|
+
if (!activityId) return { ok: false, error: { code: "TIKTOK_ERROR", message: "\u5EFA\u6D3B\u52A8\u6210\u529F\u4F46\u672A\u62FF\u5230 activity_id", retryable: false }, meta: created.meta };
|
|
163
|
+
const withProducts = await proxy("PUT", shop, `/promotion/202309/activities/${activityId}/products`, {
|
|
164
|
+
body: { ...flashProductsBody(args2), activity_id: activityId }
|
|
165
|
+
});
|
|
166
|
+
return withProducts.ok ? { ok: true, data: { activity_id: activityId, ...withProducts.data }, meta: withProducts.meta } : { ...withProducts, data: { activity_id: activityId, note: "\u6D3B\u52A8\u58F3\u5DF2\u5EFA\u4F46\u52A0\u5546\u54C1\u5931\u8D25\uFF0C\u53EF promotions deactivate \u6E05\u7406" } };
|
|
167
|
+
},
|
|
168
|
+
readback: ({ shop, args: args2 }) => proxy("POST", shop, "/promotion/202309/activities/search", { body: { status: "NOT_START", page_size: 20 } })
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "promotions deactivate",
|
|
172
|
+
summary: "\u7EC8\u6B62/\u505C\u7528\u4FC3\u9500\u6D3B\u52A8\uFF08--id \u5FC5\u586B\uFF09\u3002\u4E0D\u53EF\u9006",
|
|
173
|
+
write: true,
|
|
174
|
+
preview: ({ args: args2 }) => [{ step: "\u7EC8\u6B62\u6D3B\u52A8", method: "POST", path: `/promotion/202309/activities/${args2.id}/deactivate`, body: {} }],
|
|
175
|
+
run: ({ shop, args: args2 }) => proxy("POST", shop, `/promotion/202309/activities/${args2.id}/deactivate`, { body: {} })
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "products set-inventory",
|
|
179
|
+
summary: "\u6539 SKU \u5E93\u5B58\uFF08--product --sku --warehouse --quantity\uFF09\u3002\u5199\u64CD\u4F5C,\u9ED8\u8BA4 dry-run",
|
|
180
|
+
write: true,
|
|
181
|
+
preview: ({ args: args2 }) => [{
|
|
182
|
+
step: "\u6539\u5E93\u5B58",
|
|
183
|
+
method: "POST",
|
|
184
|
+
path: `/product/202309/products/${args2.product}/inventory/update`,
|
|
185
|
+
body: inventoryBody(args2)
|
|
186
|
+
}],
|
|
187
|
+
run: ({ shop, args: args2 }) => proxy("POST", shop, `/product/202309/products/${args2.product}/inventory/update`, { body: inventoryBody(args2) }),
|
|
188
|
+
readback: ({ shop, args: args2 }) => proxy("GET", shop, `/product/202309/products/${args2.product}`, {})
|
|
189
|
+
}
|
|
190
|
+
];
|
|
191
|
+
function inventoryBody(args2) {
|
|
192
|
+
return {
|
|
193
|
+
skus: [{
|
|
194
|
+
id: args2.sku,
|
|
195
|
+
inventory: [{ warehouse_id: args2.warehouse, quantity: Number(args2.quantity) }]
|
|
196
|
+
}]
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function flashWindow(args2) {
|
|
200
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
201
|
+
const begin = now + Number(args2["start-in-min"] ?? 15) * 60;
|
|
202
|
+
const end = begin + Number(args2.hours ?? 48) * 3600;
|
|
203
|
+
return { begin, end };
|
|
204
|
+
}
|
|
205
|
+
function flashActivityBody(args2, begin, end) {
|
|
206
|
+
return {
|
|
207
|
+
title: args2.title,
|
|
208
|
+
activity_type: "FLASHSALE",
|
|
209
|
+
product_level: "VARIATION",
|
|
210
|
+
// 指定 SKU 参与
|
|
211
|
+
begin_time: begin,
|
|
212
|
+
end_time: end
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function flashProductsBody(args2) {
|
|
216
|
+
const limit = Number(args2.limit ?? 100);
|
|
217
|
+
const perUser = Number(args2["per-user"] ?? 1);
|
|
218
|
+
return {
|
|
219
|
+
products: [{
|
|
220
|
+
id: args2.product,
|
|
221
|
+
quantity_limit: -1,
|
|
222
|
+
quantity_per_user: perUser,
|
|
223
|
+
skus: [{
|
|
224
|
+
id: args2.sku,
|
|
225
|
+
activity_price_amount: args2.price,
|
|
226
|
+
quantity_limit: limit,
|
|
227
|
+
quantity_per_user: perUser
|
|
228
|
+
}]
|
|
229
|
+
}]
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function priceBody(args2) {
|
|
233
|
+
return {
|
|
234
|
+
skus: [{ id: args2.sku, price: { amount: args2.price, currency: args2.currency ?? "MYR" } }]
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function findCommand(tokens) {
|
|
238
|
+
for (const cmd of [...COMMANDS].sort((a, b) => b.name.length - a.name.length)) {
|
|
239
|
+
const parts = cmd.name.split(" ");
|
|
240
|
+
if (parts.every((p, i) => tokens[i] === p)) {
|
|
241
|
+
return { cmd, rest: tokens.slice(parts.length) };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/index.ts
|
|
248
|
+
function parseFlags(tokens) {
|
|
249
|
+
const args2 = {};
|
|
250
|
+
const positional2 = [];
|
|
251
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
252
|
+
const t = tokens[i];
|
|
253
|
+
if (t.startsWith("--")) {
|
|
254
|
+
const key = t.slice(2);
|
|
255
|
+
const next = tokens[i + 1];
|
|
256
|
+
if (next && !next.startsWith("--")) {
|
|
257
|
+
args2[key] = next;
|
|
258
|
+
i++;
|
|
259
|
+
} else args2[key] = "true";
|
|
260
|
+
} else positional2.push(t);
|
|
261
|
+
}
|
|
262
|
+
return { args: args2, positional: positional2 };
|
|
263
|
+
}
|
|
264
|
+
function out(obj) {
|
|
265
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
266
|
+
}
|
|
267
|
+
function usage() {
|
|
268
|
+
const lines = ["tiktok <domain> <verb> --shop <shopId> [--flags]", "", "\u547D\u4EE4\uFF1A"];
|
|
269
|
+
for (const c of COMMANDS) lines.push(` ${c.name.padEnd(22)} ${c.summary}`);
|
|
270
|
+
lines.push(` ${"shops".padEnd(22)} \u67E5\u5F53\u524D\u767B\u5F55\u7528\u6237\u5DF2\u6388\u6743\u5E97\u94FA`);
|
|
271
|
+
lines.push("", "\u901A\u7528\uFF1A--shop \u5FC5\u586B\uFF08\u9664 shops\uFF09\uFF1B\u9ED8\u8BA4 JSON \u8F93\u51FA\uFF1BTIKTOK_BACKEND_URL \u6307\u5411 backend\u3002");
|
|
272
|
+
process.stderr.write(lines.join("\n") + "\n");
|
|
273
|
+
}
|
|
274
|
+
var argv = process.argv.slice(2);
|
|
275
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
276
|
+
usage();
|
|
277
|
+
process.exit(argv.length === 0 ? 1 : 0);
|
|
278
|
+
}
|
|
279
|
+
var { args, positional } = parseFlags(argv);
|
|
280
|
+
if (positional[0] === "shops") {
|
|
281
|
+
out(await listShops());
|
|
282
|
+
process.exit(0);
|
|
283
|
+
}
|
|
284
|
+
var match = findCommand(positional);
|
|
285
|
+
if (!match) {
|
|
286
|
+
out({ ok: false, error: { code: "VALIDATION", message: `\u672A\u77E5\u547D\u4EE4: ${positional.join(" ")}`, retryable: false } });
|
|
287
|
+
usage();
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
if (!args.shop) {
|
|
291
|
+
out({ ok: false, error: { code: "VALIDATION", message: "\u7F3A\u5C11 --shop\uFF08\u591A\u5E97\u663E\u5F0F\u8DEF\u7531\uFF09", retryable: false } });
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
var ctx = { shop: args.shop, args, actor: args.actor };
|
|
295
|
+
if (match.cmd.write && !("apply" in args)) {
|
|
296
|
+
const plan = match.cmd.preview?.(ctx);
|
|
297
|
+
out({
|
|
298
|
+
ok: true,
|
|
299
|
+
dry_run: true,
|
|
300
|
+
message: "\u672A\u52A0 --apply\uFF0C\u4EC5\u9884\u89C8\u3002\u786E\u8BA4\u65E0\u8BEF\u540E\u52A0 --apply \u6267\u884C\u3002",
|
|
301
|
+
plan
|
|
302
|
+
});
|
|
303
|
+
process.exit(0);
|
|
304
|
+
}
|
|
305
|
+
var env = await match.cmd.run(ctx);
|
|
306
|
+
if (match.cmd.write && env.ok && match.cmd.readback) {
|
|
307
|
+
const rb = await match.cmd.readback(ctx);
|
|
308
|
+
out({ ...env, applied: true, readback: rb.ok ? rb.data : { error: rb.error } });
|
|
309
|
+
} else {
|
|
310
|
+
out(env);
|
|
311
|
+
}
|
|
312
|
+
process.exit(env.ok ? 0 : 1);
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@optima-chat/tiktok-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TikTok Shop CLI for Optima agents — 经 tiktok-backend 操作店铺",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tiktok": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup src/index.ts --format esm --clean",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"author": "Optima Commerce Team",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.10.0",
|
|
19
|
+
"tsup": "^8.5.1",
|
|
20
|
+
"tsx": "^4.19.0",
|
|
21
|
+
"typescript": "^5.7.0"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/Optima-Chat/tiktok-api-integration.git",
|
|
29
|
+
"directory": "tiktok-cli"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
}
|
|
37
|
+
}
|