@optima-chat/tiktok-cli 0.16.1 → 0.18.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 +26 -0
  2. package/dist/index.js +206 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,6 +13,7 @@ tiktok <domain> <verb> --shop <shopId> [--flags]
13
13
  - `--shop` **必填**(多店显式路由,对应 A 轨「多店必须显式选店」教训;`auth link` 除外)
14
14
  - 默认 **JSON 输出**,统一信封 `{ ok, data?, error?, meta? }`
15
15
  - `TIKTOK_BACKEND_URL` 解析顺序:显式环境变量 > `OPTIMA_ENV` 映射(`cn-stage` → `https://tiktok-api.stage.optima.chat`;`cn-prod` → `https://tiktok-api.yzsgo.com`)> 本地默认 `http://127.0.0.1:3000`
16
+ - `OPTIMA_TRACE`(可选,COO 会话设为 `coo/<taskId>`):非空(trim 后)时每个打 backend 的请求都带 `X-Optima-Trace: <值>`,供用量按任务归因(Optima-Chat/optima-coo#645);没设 / 空白则不带。只发 backend,CLI 本就不直连第三方
16
17
 
17
18
  ## 命令(v1 只读)
18
19
 
@@ -115,6 +116,31 @@ tiktok products set-price --shop <id> --product <pid> --sku <sid> --price 29.90
115
116
 
116
117
  **秒杀字段坑**(OAS 未标、实测撞出):商品级 `quantity_limit` 在 VARIATION 模式必须 `-1`(限量由 SKU 级控制);秒杀价须**低于该 SKU 近 30 天最低成交价**且**不超折扣上限**。这些复杂度被 CLI 吸收,agent 一次成型。
117
118
 
119
+ ## 写命令参数清单(`tiktok schema --json`)
120
+
121
+ 给 Optima COO 的「写入数值守门」用(#56;optima-coo 的设计文档 `2026-10-01-optima-coo-write-value-guard-design.md` §4.2):COO 放行一条 `tiktok … --apply` 之前,按这份清单把命令里的数换算成店主口径,再和店主批过的 playbook 核对。
122
+
123
+ ```bash
124
+ tiktok schema --json > tiktok-cli@$(node -p "require('./package.json').version").json # 不需要 --shop;输出裸 JSON,不包 {ok,data}
125
+ ```
126
+
127
+ - 覆盖每条 `write: true` 的命令;每个参数恰好落在一处:`params`(带钱的,写 `semantic` 和单位:`type` / `pair` / `factor` / `means` / `scale` / `currency_flag` / `currency_default`)、`subject`(作用对象)、`unchecked`(不是钱:id、标题、小时数、库存)、`opaque`(数值藏在文件里:`--body-file`)。每条命令都认的 `--shop` / `--actor` / `--apply` / `--help` 在顶层 `global_flags`。
128
+ - 标注写在 `src/commands.ts` 每条写命令的 `values` 上(和 `flags` 挨着),`src/schema.ts` 生成并自查:漏写、一个参数写两处、钱没写币种、折扣没写 `means` ⇒ 当场抛错;`tests/schema.test.mts` 钉着。**加写命令或参数时一起改 `values`。**
129
+ - 词表(`semantic`)以 optima-coo `packages/contracts/src/value-guard.ts` 的 `VALUE_SEMANTICS` 为准。
130
+ - `version` 取自 `package.json`。COO 按版本固定清单副本,**改了任何写命令的参数就要发新版本**,否则 COO 那边按旧清单核对。
131
+ - 没有 `repeatable` 的参数:同一个参数写两次 CLI 取后者,COO 按写法错误退回;多个值用逗号连成一个(`--price A:12,B:15`)。
132
+
133
+ 数值范围(validate 里查,dry-run 与 `--apply` 共用、在发请求之前):
134
+
135
+ | 参数 | 范围 |
136
+ |---|---|
137
+ | `affiliate open-collab set-commission --rate` | 800–8000 的整数(单位百分比×100:1350 = 13.5%,8%–80%);必填 |
138
+ | `affiliate open-collab settings edit --rate` | 给了就必须是 1–10000 的整数(不超过 100%) |
139
+ | `promotions create-flashsale --hours` | 大于 0、小于 72(平台硬上限) |
140
+ | `products set-price --price`、`promotions create-flashsale --price` | 大于 0 的十进制数,最多两位小数,不要前导零和币种符号;必填 |
141
+
142
+ dry-run 的 `plan[].human` 写换算后的店主口径:改价「SKU S 售价改为 29.90 MYR」、秒杀「秒杀价 9.90 MYR,限量 100 件……持续 48 小时」、佣金「佣金率设为 13.5%(--rate 1350)」、折扣「减 85%(按原价 15% 售卖)」。
143
+
118
144
  ## 错误分类(agent 靠此决策)
119
145
 
120
146
  `error.code` + `retryable` 让 agent 判断重试还是停止:
package/dist/index.js CHANGED
@@ -129,6 +129,15 @@ function authHeaders() {
129
129
  }
130
130
  return {};
131
131
  }
132
+ function traceHeaders(env2 = process.env) {
133
+ const v = env2.OPTIMA_TRACE?.trim();
134
+ return v ? { "X-Optima-Trace": v } : {};
135
+ }
136
+ function backendFetch(url, init = {}) {
137
+ const headers = new Headers(init.headers);
138
+ for (const [k, v] of Object.entries(traceHeaders())) headers.set(k, v);
139
+ return fetch(url, { ...init, headers });
140
+ }
132
141
  function classify(status, body, shop) {
133
142
  const meta = { shop, tiktok_request_id: body?.request_id, http_status: status };
134
143
  if (status === 0 || body?.code === 0) {
@@ -148,7 +157,7 @@ async function proxy(method, shop, path, opts = {}) {
148
157
  const qs = opts.query ? "?" + new URLSearchParams(opts.query) : "";
149
158
  const url = `${BACKEND}/proxy/${shop}${path}${qs}`;
150
159
  try {
151
- const r = await fetch(url, {
160
+ const r = await backendFetch(url, {
152
161
  method,
153
162
  headers: {
154
163
  "Content-Type": "application/json",
@@ -165,7 +174,7 @@ async function proxy(method, shop, path, opts = {}) {
165
174
  }
166
175
  async function listShops() {
167
176
  try {
168
- const r = await fetch(`${BACKEND}/auth/shops`, { headers: authHeaders() });
177
+ const r = await backendFetch(`${BACKEND}/auth/shops`, { headers: authHeaders() });
169
178
  if (r.status === 401) return { ok: false, error: { code: "AUTH_EXPIRED", message: "\u672A\u767B\u5F55\u6216 token \u5931\u6548", retryable: false } };
170
179
  const body = await r.json();
171
180
  return { ok: true, data: body.shops };
@@ -176,7 +185,7 @@ async function listShops() {
176
185
  async function setShopApp(shopId, serviceId, note) {
177
186
  const token = process.env.TIKTOK_ADMIN_TOKEN;
178
187
  try {
179
- const r = await fetch(`${BACKEND}/admin/apps/shop-map`, {
188
+ const r = await backendFetch(`${BACKEND}/admin/apps/shop-map`, {
180
189
  method: "PUT",
181
190
  headers: { "Content-Type": "application/json", ...token ? { "x-admin-token": token } : {} },
182
191
  body: JSON.stringify({ shopId, serviceId, note })
@@ -190,7 +199,7 @@ async function setShopApp(shopId, serviceId, note) {
190
199
  }
191
200
  async function availableApps() {
192
201
  try {
193
- const r = await fetch(`${BACKEND}/auth/apps`, { headers: authHeaders() });
202
+ const r = await backendFetch(`${BACKEND}/auth/apps`, { headers: authHeaders() });
194
203
  if (r.status === 401) return { ok: false, error: { code: "AUTH_EXPIRED", message: "\u672A\u767B\u5F55\u6216 token \u5931\u6548", retryable: false } };
195
204
  const body = await r.json().catch(() => ({}));
196
205
  if (!r.ok || body?.error) {
@@ -203,7 +212,7 @@ async function availableApps() {
203
212
  }
204
213
  async function authLink(opts) {
205
214
  try {
206
- const r = await fetch(`${BACKEND}/auth/link`, {
215
+ const r = await backendFetch(`${BACKEND}/auth/link`, {
207
216
  method: "POST",
208
217
  headers: { "Content-Type": "application/json", ...authHeaders() },
209
218
  body: JSON.stringify({ shop: opts.shop, app: opts.app })
@@ -222,7 +231,7 @@ async function adminApps(method, opts = {}) {
222
231
  const url = `${BACKEND}/admin/apps${opts.id ? `/${opts.id}` : ""}`;
223
232
  const token = process.env.TIKTOK_ADMIN_TOKEN;
224
233
  try {
225
- const r = await fetch(url, {
234
+ const r = await backendFetch(url, {
226
235
  method,
227
236
  headers: {
228
237
  "Content-Type": "application/json",
@@ -245,7 +254,7 @@ async function adminApps(method, opts = {}) {
245
254
  async function fetchEvents(shop, query) {
246
255
  try {
247
256
  const qs = new URLSearchParams(query).toString();
248
- const r = await fetch(`${BACKEND}/events/${shop}${qs ? "?" + qs : ""}`, { headers: authHeaders() });
257
+ const r = await backendFetch(`${BACKEND}/events/${shop}${qs ? "?" + qs : ""}`, { headers: authHeaders() });
249
258
  if (r.status === 401) return { ok: false, error: { code: "AUTH_EXPIRED", message: "\u672A\u767B\u5F55\u6216 token \u5931\u6548", retryable: false } };
250
259
  if (r.status === 404) return { ok: false, error: { code: "SHOP_NOT_AUTHORIZED", message: "\u672A\u627E\u5230\u4F60\u540D\u4E0B\u7684\u5DF2\u6388\u6743\u5E97\u94FA", retryable: false }, meta: { shop } };
251
260
  const body = await r.json();
@@ -256,7 +265,7 @@ async function fetchEvents(shop, query) {
256
265
  }
257
266
  async function ackEvents(shop, body) {
258
267
  try {
259
- const r = await fetch(`${BACKEND}/events/${shop}/ack`, {
268
+ const r = await backendFetch(`${BACKEND}/events/${shop}/ack`, {
260
269
  method: "POST",
261
270
  headers: { "Content-Type": "application/json", ...authHeaders() },
262
271
  body: JSON.stringify(body)
@@ -275,7 +284,7 @@ async function uploadImage(shop, filePath, useCase) {
275
284
  const form = new FormData();
276
285
  form.append("data", new Blob([buf]), basename(filePath));
277
286
  form.append("use_case", useCase);
278
- const r = await fetch(`${BACKEND}/proxy-upload/${shop}/image`, { method: "POST", headers: authHeaders(), body: form });
287
+ const r = await backendFetch(`${BACKEND}/proxy-upload/${shop}/image`, { method: "POST", headers: authHeaders(), body: form });
279
288
  const body = await r.json().catch(() => ({}));
280
289
  if (body?.code === 0) return { ok: true, data: { uri: body.data?.uri, url: body.data?.url, use_case: useCase }, meta: { shop } };
281
290
  return { ok: false, error: { code: r.status === 401 ? "AUTH_EXPIRED" : "TIKTOK_ERROR", message: body?.message ?? body?.error ?? `HTTP ${r.status}`, retryable: false }, meta: { shop } };
@@ -354,6 +363,13 @@ var ADD_TEXT = {
354
363
  AN3_MIS: "\u503C\u4E0D\u7B26\u7684\u6309\u8BFB\u56DE\u7684\u503C\u751F\u6548\uFF0C\u6539\u4EF7 / \u6539\u6298\u6263\u4E0D\u5728\u672C\u547D\u4EE4\u8303\u56F4\u3002",
355
364
  AN4: (shop, id) => `\u52A0\u5546\u54C1\u5931\u8D25\uFF0C\u56DE\u8BFB\u4E5F\u5931\u8D25\u4E86\uFF0C\u67E5\u4E0D\u5230\u6D3B\u52A8 ${id} \u91CC\u672C\u6B21\u7684 SKU \u6709\u6CA1\u6709\u6302\u4E0A\u3002\u5148\u7528 \`tiktok promotions get --shop ${shop} --id ${id}\` \u6838\u5BF9\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u91CD\u8DD1\uFF08\u5DF2\u6302\u4E0A\u7684\u4F1A\u5728\u9884\u8BFB\u90A3\u6B65\u88AB\u8BC6\u522B\uFF0C\u91CD\u8DD1\u4E0D\u4F1A\u91CD\u590D\u52A0\uFF09\u3002`
356
365
  };
366
+ var DISCOUNT_VALUE_PARAMS = {
367
+ // 一口价不带币种,平台按店铺本币收;本 CLI 只接马来站 ⇒ MYR。PRICE_ITEM 只收两位小数
368
+ price: { semantic: "promo_price", type: "pairs", pair: ["sku_id", "decimal"], scale: 2, currency_default: "MYR" },
369
+ // PCT_ITEM:1–99 的整数,「减 N%」(85 = 按原价 15% 卖)
370
+ percent: { semantic: "discount_pct", type: "pairs", pair: ["sku_id", "int"], means: "off" }
371
+ };
372
+ var COMMISSION_RATE = { semantic: "commission_pct", type: "int", factor: 0.01 };
357
373
  var COMMANDS = [
358
374
  {
359
375
  name: "orders list",
@@ -478,6 +494,7 @@ var COMMANDS = [
478
494
  summary: "\u6279\u51C6\u53D6\u6D88\u5355\uFF08--id cancel_id\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u6279\u3002\u4E0D\u53EF\u9006",
479
495
  flags: ["id"],
480
496
  write: true,
497
+ values: { unchecked: ["id"] },
481
498
  preview: ({ args: args2 }) => [{
482
499
  step: "\u6279\u51C6\u53D6\u6D88\u5355",
483
500
  method: "POST",
@@ -491,6 +508,7 @@ var COMMANDS = [
491
508
  summary: "\u9A73\u56DE\u53D6\u6D88\u5355\uFF08--id --reason\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u9A73\u3002\u4E0D\u53EF\u9006",
492
509
  flags: ["id", "reason"],
493
510
  write: true,
511
+ values: { unchecked: ["id", "reason"] },
494
512
  preview: ({ args: args2 }) => [{
495
513
  step: "\u9A73\u56DE\u53D6\u6D88\u5355",
496
514
  method: "POST",
@@ -509,6 +527,7 @@ var COMMANDS = [
509
527
  summary: "\u6279\u51C6\u9000\u8D27\u5355\uFF08--id return_id\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u6279\u3002\u4E0D\u53EF\u9006",
510
528
  flags: ["id"],
511
529
  write: true,
530
+ values: { unchecked: ["id"] },
512
531
  preview: ({ args: args2 }) => [{
513
532
  step: "\u6279\u51C6\u9000\u8D27\u5355",
514
533
  method: "POST",
@@ -522,6 +541,7 @@ var COMMANDS = [
522
541
  summary: "\u9A73\u56DE\u9000\u8D27\u5355\uFF08--id --reason\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u9A73\u3002\u4E0D\u53EF\u9006",
523
542
  flags: ["id", "reason"],
524
543
  write: true,
544
+ values: { unchecked: ["id", "reason"] },
525
545
  preview: ({ args: args2 }) => [{
526
546
  step: "\u9A73\u56DE\u9000\u8D27\u5355",
527
547
  method: "POST",
@@ -540,6 +560,7 @@ var COMMANDS = [
540
560
  summary: "\u4E3A\u8BA2\u5355\u5EFA\u5305\u88F9\uFF08--order id --body-file pkg.json\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u5EFA\u3002\u4EC5\u76F4\u53D1\u5546\u5BB6\uFF08\u7269\u6D41\u9ED8\u8BA4 ERP \u7BA1\uFF09",
541
561
  flags: ["body-file", "order"],
542
562
  write: true,
563
+ values: { unchecked: ["order"], opaque: ["body-file"] },
543
564
  preview: ({ args: args2 }) => [{
544
565
  step: "\u5EFA\u5305\u88F9",
545
566
  method: "POST",
@@ -558,6 +579,7 @@ var COMMANDS = [
558
579
  summary: "\u53D1\u8D27\uFF08--package id --body-file ship.json\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u53D1\u3002\u4E0D\u53EF\u9006\uFF0C\u4EC5\u76F4\u53D1\u5546\u5BB6",
559
580
  flags: ["body-file", "package"],
560
581
  write: true,
582
+ values: { unchecked: ["package"], opaque: ["body-file"] },
561
583
  preview: ({ args: args2 }) => [{
562
584
  step: "\u53D1\u8D27",
563
585
  method: "POST",
@@ -590,6 +612,7 @@ var COMMANDS = [
590
612
  summary: "\u6807\u4E8B\u4EF6\u5DF2\u5904\u7406\uFF08--ids id1,id2 \u6216 --before <ts>\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u6807",
591
613
  flags: ["before", "ids"],
592
614
  write: true,
615
+ values: { unchecked: ["before", "ids"] },
593
616
  preview: ({ args: args2 }) => [{
594
617
  step: "\u6807\u4E8B\u4EF6\u5DF2\u5904\u7406",
595
618
  method: "POST",
@@ -946,28 +969,45 @@ var COMMANDS = [
946
969
  // ===== 写操作(Phase 2):默认 --dry-run,--apply 才真发,成功后回读 =====
947
970
  {
948
971
  name: "products set-price",
949
- summary: "\u6539 SKU \u552E\u4EF7\uFF08--product --sku --price [--currency MYR]\uFF09\u3002\u5199\u64CD\u4F5C,\u9ED8\u8BA4 dry-run",
972
+ summary: "\u6539 SKU \u552E\u4EF7\uFF08--product --sku --price [--currency MYR]\uFF0C\u4EF7\u683C\u5927\u4E8E 0\u3001\u6700\u591A\u4E24\u4F4D\u5C0F\u6570\uFF09\u3002\u5199\u64CD\u4F5C,\u9ED8\u8BA4 dry-run",
950
973
  flags: ["currency", "price", "product", "sku"],
951
974
  write: true,
975
+ validate: validateSetPrice,
976
+ values: {
977
+ subject: { flag: "sku", kind: "sku_id" },
978
+ params: {
979
+ price: { semantic: "list_price", type: "decimal", scale: 2, currency_flag: "currency", currency_default: "MYR" },
980
+ currency: { semantic: "currency" }
981
+ },
982
+ unchecked: ["product"]
983
+ },
952
984
  preview: ({ args: args2 }) => [{
953
985
  step: "\u6539\u4EF7",
954
986
  method: "POST",
955
987
  path: `/product/202309/products/${args2.product}/prices/update`,
956
- body: priceBody(args2)
988
+ body: priceBody(args2),
989
+ human: priceHuman(args2)
957
990
  }],
958
991
  run: ({ shop, args: args2 }) => proxy("POST", shop, `/product/202309/products/${args2.product}/prices/update`, { body: priceBody(args2) }),
959
992
  readback: ({ shop, args: args2 }) => proxy("GET", shop, `/product/202309/products/${args2.product}`, {})
960
993
  },
961
994
  {
962
995
  name: "promotions create-flashsale",
963
- summary: "\u5EFA\u79D2\u6740\u6D3B\u52A8\uFF08--title --product --sku --price [--limit] [--hours 48]\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u771F\u5EFA\u3002\u4E24\u6B65:\u5EFA\u58F3+\u52A0\u54C1",
996
+ summary: "\u5EFA\u79D2\u6740\u6D3B\u52A8\uFF08--title --product --sku --price [--limit] [--hours 48]\uFF1B--price \u5927\u4E8E 0\u3001\u6700\u591A\u4E24\u4F4D\u5C0F\u6570\uFF0C--hours \u5C0F\u4E8E 72\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u771F\u5EFA\u3002\u4E24\u6B65:\u5EFA\u58F3+\u52A0\u54C1",
964
997
  flags: ["hours", "limit", "per-user", "price", "product", "sku", "start-in-min", "title"],
965
998
  write: true,
999
+ validate: validateFlashsale,
1000
+ values: {
1001
+ subject: { flag: "sku", kind: "sku_id" },
1002
+ // 秒杀价不带币种,平台按店铺本币收;本 CLI 只接马来站 ⇒ MYR
1003
+ params: { price: { semantic: "promo_price", type: "decimal", scale: 2, currency_default: "MYR" } },
1004
+ unchecked: ["hours", "limit", "per-user", "product", "start-in-min", "title"]
1005
+ },
966
1006
  preview: ({ args: args2 }) => {
967
1007
  const { begin, end } = flashWindow(args2);
968
1008
  return [
969
1009
  { step: "1/2 \u5EFA\u6D3B\u52A8\u58F3", method: "POST", path: "/promotion/202309/activities", body: flashActivityBody(args2, begin, end) },
970
- { step: "2/2 \u52A0\u5546\u54C1+\u79D2\u6740\u4EF7", method: "PUT", path: "/promotion/202309/activities/{activity_id}/products", body: flashProductsBody(args2) }
1010
+ { step: "2/2 \u52A0\u5546\u54C1+\u79D2\u6740\u4EF7", method: "PUT", path: "/promotion/202309/activities/{activity_id}/products", body: flashProductsBody(args2), human: flashHuman(args2) }
971
1011
  ];
972
1012
  },
973
1013
  run: async ({ shop, args: args2 }) => {
@@ -988,6 +1028,7 @@ var COMMANDS = [
988
1028
  summary: "\u5EFA\u5546\u54C1\u6298\u6263\u6D3B\u52A8 FIXED_PRICE\uFF08--price \u4E00\u53E3\u4EF7\uFF09\u6216 DIRECT_DISCOUNT\uFF08--percent \u51CFN%\uFF09\uFF08--title [--product] --price <skuId>:<\u4E00\u53E3\u4EF7>|<productId>/<skuId>:<\u4E00\u53E3\u4EF7>[,...] \u6216 --percent <skuId>:<\u51CFN%>|<productId>/<skuId>:<\u51CFN%>[,...] [--days 180]\uFF09\u3002--percent 85 = \u51CF85%\u3001\u6309\u539F\u4EF715%\u5356\uFF08\u625385\u6298\u8981\u4F2015\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u771F\u5EFA\u3002\u4E24\u6B65:\u5EFA\u58F3+\u52A0\u54C1\uFF0C\u53EF\u591A\u5546\u54C1",
989
1029
  flags: ["days", "percent", "price", "product", "title"],
990
1030
  write: true,
1031
+ values: { params: DISCOUNT_VALUE_PARAMS, unchecked: ["days", "product", "title"] },
991
1032
  validate: validateCreateDiscount,
992
1033
  preview: ({ args: args2 }) => {
993
1034
  const { begin, end } = discountWindow(args2);
@@ -1062,6 +1103,7 @@ var COMMANDS = [
1062
1103
  summary: ADD_TEXT.SUM_ADD,
1063
1104
  flags: ["id", "percent", "price", "product"],
1064
1105
  write: true,
1106
+ values: { params: DISCOUNT_VALUE_PARAMS, unchecked: ["id", "product"] },
1065
1107
  validate: validateAddProducts,
1066
1108
  // 🔴 命令块只字面读 args.id / args.product,值旗标经 discountValueFlag / discountValueRaw(它们字面读 args.percent / args.price);
1067
1109
  // ⛔ 不得提到 discountHuman / validateCreateDiscount / discountActivityBody / discountWindow(它们读 days / title,本命令没有)
@@ -1081,6 +1123,7 @@ var COMMANDS = [
1081
1123
  summary: "\u7EC8\u6B62/\u505C\u7528\u4FC3\u9500\u6D3B\u52A8\uFF08--id \u5FC5\u586B\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u7EC8\u6B62\u3002\u4E0D\u53EF\u9006",
1082
1124
  flags: ["id"],
1083
1125
  write: true,
1126
+ values: { unchecked: ["id"] },
1084
1127
  preview: ({ args: args2 }) => [{ step: "\u7EC8\u6B62\u6D3B\u52A8", method: "POST", path: `/promotion/202309/activities/${args2.id}/deactivate`, body: {} }],
1085
1128
  run: ({ shop, args: args2 }) => proxy("POST", shop, `/promotion/202309/activities/${args2.id}/deactivate`, { body: {} })
1086
1129
  },
@@ -1089,6 +1132,7 @@ var COMMANDS = [
1089
1132
  summary: "\u6539 SKU \u5E93\u5B58\uFF08--product --sku --warehouse --quantity\uFF09\u3002\u5199\u64CD\u4F5C,\u9ED8\u8BA4 dry-run",
1090
1133
  flags: ["product", "quantity", "sku", "warehouse"],
1091
1134
  write: true,
1135
+ values: { unchecked: ["product", "quantity", "sku", "warehouse"] },
1092
1136
  preview: ({ args: args2 }) => [{
1093
1137
  step: "\u6539\u5E93\u5B58",
1094
1138
  method: "POST",
@@ -1103,6 +1147,7 @@ var COMMANDS = [
1103
1147
  summary: "\u5EFA\u54C1\uFF08--body-file [--apply]\uFF09\u3002dry-run \u8DD1 listing_check \u9884\u68C0\uFF1B\u5199\u64CD\u4F5C\u3002\u26A0\uFE0F --apply \u5373\u5EFA\u5B8C\u76F4\u63A5\u4E0A\u67B6\u9001\u5BA1\uFF1B\u7528\u6237\u8981\u5B58\u8349\u7A3F\u5C31\u505C\u624B\u95EE\u7528\u6237\uFF08#1958\uFF09",
1104
1148
  flags: ["body-file"],
1105
1149
  write: true,
1150
+ values: { opaque: ["body-file"] },
1106
1151
  preview: () => [{ step: "listing_check \u9884\u68C0 \u2192 \u5EFA\u54C1", method: "POST", path: "/product/202309/products/listing_check", body: void 0 }],
1107
1152
  precheck: ({ shop, args: args2 }) => proxy("POST", shop, "/product/202309/products/listing_check", { body: readBodyFile(args2["body-file"]) }),
1108
1153
  run: async ({ shop, args: args2 }) => {
@@ -1118,6 +1163,7 @@ var COMMANDS = [
1118
1163
  summary: "\u7ED9\u5546\u54C1\u52A0\u89C4\u683C SKU\uFF08--product --body-file skus-delta.json [--apply]\uFF09\u3002\u26A0\uFE0F --apply \u5373\u9001\u5BA1\u4E0A\u67B6\uFF1B\u7528\u6237\u8981\u5B58\u8349\u7A3F\u5C31\u505C\u624B\u95EE\u7528\u6237\uFF08#1958\uFF09",
1119
1164
  flags: ["body-file", "product"],
1120
1165
  write: true,
1166
+ values: { unchecked: ["product"], opaque: ["body-file"] },
1121
1167
  preview: () => [{ step: "\u8BFB\u73B0\u6709 skus \u2192 append \u2192 partial_edit\u3002\u26A0\uFE0F \u9ED8\u8BA4 save_mode=LISTING\uFF1A--apply \u5373\u9001\u5BA1\u4E0A\u67B6\uFF08DRAFT \u5546\u54C1\u4F1A\u88AB\u63D0\u4EA4\uFF09\uFF1B\u672C\u547D\u4EE4\u81EA\u6784 body\uFF0Cbody-file \u91CC\u7684 save_mode \u4E0D\u900F\u4F20\u3001\u5199\u4E86\u4E5F\u65E0\u6548\u3002\u82E5\u7528\u6237\u8981\u6C42\u5B58\u8349\u7A3F/\u522B\u63D0\u4EA4 \u2192 \u4E0D\u8981 --apply\uFF0C\u505C\u624B\u95EE\u7528\u6237\uFF08#1958\uFF09", method: "POST", path: "/product/202309/products/{id}/partial_edit" }],
1122
1168
  run: async ({ shop, args: args2 }) => {
1123
1169
  const cur = await proxy("GET", shop, `/product/202309/products/${args2.product}`, {});
@@ -1132,6 +1178,8 @@ var COMMANDS = [
1132
1178
  summary: "\u6574\u7EC4\u6362\u4E3B\u56FE\uFF08--product --body-file images.json [--apply]\uFF09\u3002\u26A0\uFE0F --apply \u5373\u9001\u5BA1\u4E0A\u67B6\uFF1B\u7528\u6237\u8981\u5B58\u8349\u7A3F\u5C31\u505C\u624B\u95EE\u7528\u6237\uFF08#1958\uFF09",
1133
1179
  flags: ["body-file", "product"],
1134
1180
  write: true,
1181
+ values: { unchecked: ["body-file", "product"] },
1182
+ // 只从文件里取 main_images,带不进价格
1135
1183
  preview: () => [{ step: "main_images \u6574\u7EC4\u66FF\u6362 \u2192 partial_edit\u3002\u26A0\uFE0F \u9ED8\u8BA4 save_mode=LISTING\uFF1A--apply \u5373\u9001\u5BA1\u4E0A\u67B6\uFF08DRAFT \u5546\u54C1\u4F1A\u88AB\u63D0\u4EA4\uFF09\uFF1B\u672C\u547D\u4EE4\u81EA\u6784 body\uFF0Cbody-file \u91CC\u7684 save_mode \u4E0D\u900F\u4F20\u3001\u5199\u4E86\u4E5F\u65E0\u6548\u3002\u82E5\u7528\u6237\u8981\u6C42\u5B58\u8349\u7A3F/\u522B\u63D0\u4EA4 \u2192 \u4E0D\u8981 --apply\uFF0C\u505C\u624B\u95EE\u7528\u6237\uFF08#1958\uFF09", method: "POST", path: "/product/202309/products/{id}/partial_edit" }],
1136
1184
  run: ({ shop, args: args2 }) => proxy("POST", shop, `/product/202309/products/${args2.product}/partial_edit`, { body: { main_images: readBodyFile(args2["body-file"]).main_images } }),
1137
1185
  readback: ({ shop, args: args2 }) => proxy("GET", shop, `/product/202309/products/${args2.product}`, {})
@@ -1141,6 +1189,7 @@ var COMMANDS = [
1141
1189
  summary: "\u6539\u5546\u54C1\u5B57\u6BB5\uFF08--product --body-file patch.json [--apply]\uFF09\u3002\u26A0\uFE0F --apply \u5373\u9001\u5BA1\u4E0A\u67B6\uFF1B\u7528\u6237\u8981\u5B58\u8349\u7A3F\u5C31\u505C\u624B\u95EE\u7528\u6237\uFF08#1958\uFF09",
1142
1190
  flags: ["body-file", "product"],
1143
1191
  write: true,
1192
+ values: { unchecked: ["product"], opaque: ["body-file"] },
1144
1193
  preview: ({ args: args2 }) => [{ step: "partial_edit\uFF08\u5D4C\u5957\u5B57\u6BB5\u672A\u5217\u5B50\u9879\u4F1A\u88AB\u6E05\u7A7A\uFF09\u3002\u26A0\uFE0F \u9ED8\u8BA4 save_mode=LISTING\uFF1A--apply \u5373\u9001\u5BA1\u4E0A\u67B6\uFF08DRAFT \u5546\u54C1\u4F1A\u88AB\u63D0\u4EA4\uFF09\u3002\u82E5\u7528\u6237\u8981\u6C42\u5B58\u8349\u7A3F/\u522B\u63D0\u4EA4 \u2192 \u4E0D\u8981 --apply\uFF0C\u505C\u624B\u95EE\u7528\u6237\uFF08#1958\uFF09", method: "POST", path: `/product/202309/products/${args2.product}/partial_edit`, body: readBodyFile(args2["body-file"]) }],
1145
1194
  run: ({ shop, args: args2 }) => proxy("POST", shop, `/product/202309/products/${args2.product}/partial_edit`, { body: readBodyFile(args2["body-file"]) }),
1146
1195
  readback: ({ shop, args: args2 }) => proxy("GET", shop, `/product/202309/products/${args2.product}`, {})
@@ -1248,6 +1297,7 @@ var COMMANDS = [
1248
1297
  summary: "\u53D1\u5B9A\u5411\u9080\u7EA6\uFF08--body-file collab.json\uFF0C\u542B creator_user_open_ids[]/products[]/free_sample_rule\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u53D1",
1249
1298
  flags: ["body-file"],
1250
1299
  write: true,
1300
+ values: { opaque: ["body-file"] },
1251
1301
  preview: ({ args: args2 }) => [{
1252
1302
  step: "\u53D1\u5B9A\u5411\u9080\u7EA6",
1253
1303
  method: "POST",
@@ -1267,6 +1317,7 @@ var COMMANDS = [
1267
1317
  summary: "\u6539\u5B9A\u5411\u9080\u7EA6\uFF08--id --body-file collab.json\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u63D0\u4EA4",
1268
1318
  flags: ["body-file", "id"],
1269
1319
  write: true,
1320
+ values: { unchecked: ["id"], opaque: ["body-file"] },
1270
1321
  preview: ({ args: args2 }) => [{
1271
1322
  step: "\u6539\u5B9A\u5411\u9080\u7EA6",
1272
1323
  method: "PUT",
@@ -1283,6 +1334,7 @@ var COMMANDS = [
1283
1334
  summary: "\u64A4\u56DE\u5B9A\u5411\u9080\u7EA6\uFF08--id \u5FC5\u586B\uFF0C\u64A4\u56DE\u5B89\u5168\u7F51\uFF09\u3002\u4E0D\u53EF\u9006\uFF0C\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u53D1",
1284
1335
  flags: ["id"],
1285
1336
  write: true,
1337
+ values: { unchecked: ["id"] },
1286
1338
  preview: ({ args: args2 }) => [{
1287
1339
  step: "\u64A4\u56DE\u5B9A\u5411\u9080\u7EA6",
1288
1340
  method: "DELETE",
@@ -1296,6 +1348,7 @@ var COMMANDS = [
1296
1348
  summary: "\u751F\u6210\u5B9A\u5411\u9080\u7EA6\u5206\u4EAB\u94FE\u63A5\uFF08--id \u5FC5\u586B\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u53D1",
1297
1349
  flags: ["id"],
1298
1350
  write: true,
1351
+ values: { unchecked: ["id"] },
1299
1352
  preview: ({ args: args2 }) => [{
1300
1353
  step: "\u751F\u6210\u9080\u7EA6\u94FE\u63A5",
1301
1354
  method: "POST",
@@ -1309,6 +1362,7 @@ var COMMANDS = [
1309
1362
  summary: "\u5F00\u8FBE\u4EBA\u79C1\u4FE1\u4F1A\u8BDD\uFF08--creator \u5FC5\u586B\uFF0Ccreator_open_id\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u53D1",
1310
1363
  flags: ["creator"],
1311
1364
  write: true,
1365
+ values: { unchecked: ["creator"] },
1312
1366
  preview: ({ args: args2 }) => [{
1313
1367
  step: "\u5F00\u79C1\u4FE1\u4F1A\u8BDD",
1314
1368
  method: "POST",
@@ -1324,6 +1378,7 @@ var COMMANDS = [
1324
1378
  summary: "\u7ED9\u8FBE\u4EBA\u53D1\u79C1\u4FE1\uFF08--conversation --content [--msg-type TEXT]\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u53D1",
1325
1379
  flags: ["content", "conversation", "msg-type"],
1326
1380
  write: true,
1381
+ values: { unchecked: ["content", "conversation", "msg-type"] },
1327
1382
  preview: ({ args: args2 }) => [{
1328
1383
  step: "\u53D1\u79C1\u4FE1",
1329
1384
  method: "POST",
@@ -1340,6 +1395,7 @@ var COMMANDS = [
1340
1395
  flags: ["id", "reject-reason", "result"],
1341
1396
  listKey: "sample_applications",
1342
1397
  write: true,
1398
+ values: { unchecked: ["id", "reject-reason", "result"] },
1343
1399
  preview: ({ args: args2 }) => [{
1344
1400
  step: "\u5BA1\u6279\u6837\u54C1\u7533\u8BF7",
1345
1401
  method: "POST",
@@ -1360,11 +1416,14 @@ var COMMANDS = [
1360
1416
  summary: "\u6309\u54C1\u8BBE\u516C\u5F00\u5408\u4F5C\u4F63\u91D1\u7387\uFF08--product product_id --rate\uFF0C\u5355\u4F4D=\u767E\u5206\u6BD4\xD7100\uFF0C\u5982 1350=13.5%\uFF0C\u8303\u56F4 800-8000\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u5B58",
1361
1417
  flags: ["product", "rate"],
1362
1418
  write: true,
1419
+ validate: validateCommissionRate,
1420
+ values: { subject: { flag: "product", kind: "product_id" }, params: { rate: COMMISSION_RATE } },
1363
1421
  preview: ({ args: args2 }) => [{
1364
1422
  step: "\u8BBE\u6309\u54C1\u516C\u5F00\u5408\u4F5C\u4F63\u91D1",
1365
1423
  method: "POST",
1366
1424
  path: "/affiliate_seller/202412/open_collaborations",
1367
- body: { product_id: args2.product, commission_rate: Number(args2.rate) }
1425
+ body: { product_id: args2.product, commission_rate: Number(args2.rate) },
1426
+ human: `\u5546\u54C1 ${args2.product} \u7684\u516C\u5F00\u5408\u4F5C\u4F63\u91D1\u7387\u8BBE\u4E3A ${rateHuman(args2.rate)}`
1368
1427
  }],
1369
1428
  run: ({ shop, args: args2 }) => proxy("POST", shop, "/affiliate_seller/202412/open_collaborations", {
1370
1429
  body: { product_id: args2.product, commission_rate: Number(args2.rate) }
@@ -1379,6 +1438,7 @@ var COMMANDS = [
1379
1438
  summary: "\u79FB\u9664\u54C1\u7684\u516C\u5F00\u5408\u4F5C\uFF08--product product_id\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u53D1",
1380
1439
  flags: ["product"],
1381
1440
  write: true,
1441
+ values: { unchecked: ["product"] },
1382
1442
  preview: ({ args: args2 }) => [{
1383
1443
  step: "\u79FB\u9664\u516C\u5F00\u5408\u4F5C",
1384
1444
  method: "DELETE",
@@ -1389,14 +1449,17 @@ var COMMANDS = [
1389
1449
  },
1390
1450
  {
1391
1451
  name: "affiliate open-collab settings edit",
1392
- summary: "\u6539\u516C\u5F00\u5408\u4F5C\u5168\u5C40\u9ED8\u8BA4\uFF08\u81EA\u52A8\u65B0\u589E\u54C1\u4F63\u91D1\uFF0C--rate --enable true|false\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u5B58",
1452
+ summary: "\u6539\u516C\u5F00\u5408\u4F5C\u5168\u5C40\u9ED8\u8BA4\uFF08\u81EA\u52A8\u65B0\u589E\u54C1\u4F63\u91D1\uFF0C--rate --enable true|false\uFF1B--rate \u5355\u4F4D=\u767E\u5206\u6BD4\xD7100\uFF0C1\u201310000 \u7684\u6574\u6570\uFF09\u3002\u9ED8\u8BA4 dry-run\uFF0C--apply \u624D\u5B58",
1393
1453
  flags: ["enable", "rate"],
1394
1454
  write: true,
1455
+ validate: validateSettingsRate,
1456
+ values: { params: { rate: COMMISSION_RATE }, unchecked: ["enable"] },
1395
1457
  preview: ({ args: args2 }) => [{
1396
1458
  step: "\u6539\u5168\u5C40\u9ED8\u8BA4",
1397
1459
  method: "POST",
1398
1460
  path: "/affiliate_seller/202405/open_collaboration_settings",
1399
- body: { auto_add_product: { commission_rate: Number(args2.rate), enable: args2.enable === "true" } }
1461
+ body: { auto_add_product: { commission_rate: Number(args2.rate), enable: args2.enable === "true" } },
1462
+ human: `\u65B0\u4E0A\u67B6\u7684\u5546\u54C1${args2.enable === "true" ? "\u81EA\u52A8\u52A0\u5165" : "\u4E0D\u81EA\u52A8\u52A0\u5165"}\u516C\u5F00\u5408\u4F5C\uFF0C\u9ED8\u8BA4\u4F63\u91D1\u7387 ${rateHuman(args2.rate)}`
1400
1463
  }],
1401
1464
  run: ({ shop, args: args2 }) => proxy("POST", shop, "/affiliate_seller/202405/open_collaboration_settings", {
1402
1465
  body: { auto_add_product: { commission_rate: Number(args2.rate), enable: args2.enable === "true" } }
@@ -1515,6 +1578,42 @@ function priceBody(args2) {
1515
1578
  skus: [{ id: args2.sku, price: { amount: args2.price, currency: args2.currency ?? "MYR" } }]
1516
1579
  };
1517
1580
  }
1581
+ var PRICE_VALUE = /^(0|[1-9]\d*)(\.\d{1,2})?$/;
1582
+ var priceOk = (raw) => raw !== void 0 && PRICE_VALUE.test(raw) && Number(raw) > 0;
1583
+ var priceMessage = (raw) => `--price \u5FC5\u987B\u662F\u5927\u4E8E 0 \u7684\u5341\u8FDB\u5236\u6570\uFF0C\u6700\u591A\u4E24\u4F4D\u5C0F\u6570\u3001\u4E0D\u8981\u524D\u5BFC\u96F6\u548C\u5E01\u79CD\u7B26\u53F7\uFF08\u5982 29.90\uFF09\uFF1B\u6536\u5230\u300C${raw ?? "\uFF08\u6CA1\u4F20\uFF09"}\u300D`;
1584
+ function validateSetPrice({ args: args2 }) {
1585
+ return priceOk(args2.price) ? { ok: true } : { ok: false, message: priceMessage(args2.price) };
1586
+ }
1587
+ function validateFlashsale({ args: args2 }) {
1588
+ if (!priceOk(args2.price)) return { ok: false, message: priceMessage(args2.price) };
1589
+ if (args2.hours !== void 0) {
1590
+ const n = Number(args2.hours);
1591
+ if (!/^\d+(\.\d+)?$/.test(args2.hours) || !(n > 0 && n < 72)) {
1592
+ return { ok: false, message: `--hours \u662F\u79D2\u6740\u6301\u7EED\u7684\u5C0F\u65F6\u6570\uFF0C\u5FC5\u987B\u5927\u4E8E 0\u3001\u5C0F\u4E8E 72\uFF08\u5E73\u53F0\u786C\u4E0A\u9650\uFF0C\u9ED8\u8BA4 48\uFF09\uFF1B\u6536\u5230\u300C${args2.hours}\u300D` };
1593
+ }
1594
+ }
1595
+ return { ok: true };
1596
+ }
1597
+ function rateProblem(raw, min, max) {
1598
+ if (raw !== void 0 && /^\d+$/.test(raw) && Number(raw) >= min && Number(raw) <= max) return null;
1599
+ return `--rate \u7684\u5355\u4F4D\u662F\u300C\u767E\u5206\u6BD4\xD7100\u300D\uFF081350 = 13.5%\uFF09\uFF0C\u5FC5\u987B\u662F ${min}\u2013${max} \u7684\u6574\u6570\uFF08${min / 100}%\u2013${max / 100}%\uFF09\uFF1B\u6536\u5230\u300C${raw ?? "\uFF08\u6CA1\u4F20\uFF09"}\u300D\u3002\u5E97\u4E3B\u8BF4\u300C\u4F63\u91D1 13.5%\u300D\u8981\u4F20 1350`;
1600
+ }
1601
+ function validateCommissionRate({ args: args2 }) {
1602
+ const p = rateProblem(args2.rate, 800, 8e3);
1603
+ return p ? { ok: false, message: p } : { ok: true };
1604
+ }
1605
+ function validateSettingsRate({ args: args2 }) {
1606
+ if (args2.rate === void 0) return { ok: true };
1607
+ const p = rateProblem(args2.rate, 1, 1e4);
1608
+ return p ? { ok: false, message: p } : { ok: true };
1609
+ }
1610
+ function priceHuman(args2) {
1611
+ return `\u5546\u54C1 ${args2.product} \u7684 SKU ${args2.sku} \u552E\u4EF7\u6539\u4E3A ${args2.price} ${args2.currency ?? "MYR"}`;
1612
+ }
1613
+ function flashHuman(args2) {
1614
+ return `\u5546\u54C1 ${args2.product} \u7684 SKU ${args2.sku} \u79D2\u6740\u4EF7 ${args2.price} MYR\uFF0C\u9650\u91CF ${args2.limit ?? 100} \u4EF6\u3001\u6BCF\u4EBA\u9650\u8D2D ${args2["per-user"] ?? 1} \u4EF6\uFF1B${args2["start-in-min"] ?? 15} \u5206\u949F\u540E\u5F00\u59CB\uFF0C\u6301\u7EED ${args2.hours ?? 48} \u5C0F\u65F6`;
1615
+ }
1616
+ var rateHuman = (raw) => `${Number(raw) / 100}%\uFF08--rate ${raw}\uFF09`;
1518
1617
  function findCommand(tokens) {
1519
1618
  for (const cmd of [...COMMANDS].sort((a, b) => b.name.length - a.name.length)) {
1520
1619
  const parts = cmd.name.split(" ");
@@ -2007,6 +2106,86 @@ async function runAddProducts(shop, id, flag, raw, product) {
2007
2106
  return { ok: true, data, meta: put.meta };
2008
2107
  }
2009
2108
 
2109
+ // src/schema.ts
2110
+ import { readFileSync as readFileSync3 } from "fs";
2111
+ var VALUE_SEMANTICS = [
2112
+ "list_price",
2113
+ // SKU 挂牌价
2114
+ "promo_price",
2115
+ // 促销售价:折扣一口价、秒杀价
2116
+ "campaign_price",
2117
+ // 平台活动报价
2118
+ "discount_pct",
2119
+ // 降幅百分数(means 说是「减 N%」还是「按 N% 卖」)
2120
+ "ad_target_roi",
2121
+ // GMV Max 目标 ROI
2122
+ "ad_daily_budget",
2123
+ // GMV Max 日预算
2124
+ "commission_pct",
2125
+ // 达人佣金百分数
2126
+ "currency"
2127
+ // 同一条写里金额的币种
2128
+ ];
2129
+ var MONEY = ["list_price", "promo_price", "campaign_price", "ad_daily_budget"];
2130
+ function cliVersion() {
2131
+ return JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8")).version;
2132
+ }
2133
+ var dash = (f) => `--${f}`;
2134
+ var sorted = (xs) => [...xs].sort().map(dash);
2135
+ function checkParam(where, p, params, problems) {
2136
+ if (!VALUE_SEMANTICS.includes(p.semantic)) problems.push(`${where}\uFF1Asemantic ${JSON.stringify(p.semantic)} \u4E0D\u5728\u8BCD\u8868\u91CC`);
2137
+ if (p.semantic === "currency") {
2138
+ if (Object.keys(p).some((k) => k !== "semantic" && k !== "type") || p.type !== void 0 && p.type !== "string") problems.push(`${where}\uFF1A\u5E01\u79CD\u53C2\u6570\u53EA\u5199 semantic`);
2139
+ return;
2140
+ }
2141
+ if (MONEY.includes(p.semantic) && p.currency_flag === void 0 && p.currency_default === void 0) problems.push(`${where}\uFF1A\u94B1\u8981\u5199\u5E01\u79CD\uFF08currency_flag \u6216 currency_default\uFF09`);
2142
+ if (!MONEY.includes(p.semantic) && (p.currency_flag !== void 0 || p.currency_default !== void 0)) problems.push(`${where}\uFF1A\u53EA\u6709\u91D1\u989D\u624D\u6709\u5E01\u79CD`);
2143
+ if (p.semantic === "discount_pct" !== (p.means !== void 0)) problems.push(`${where}\uFF1A\u964D\u5E45\u5FC5\u987B\u5199 means\uFF0C\u522B\u7684\u4E0D\u8BB8\u5199`);
2144
+ if (p.round !== void 0 && p.scale === void 0) problems.push(`${where}\uFF1Around \u8981\u548C scale \u4E00\u8D77\u5199`);
2145
+ if (p.type === "pairs" !== (p.pair !== void 0)) problems.push(`${where}\uFF1Apairs \u548C pair \u8981\u4E00\u8D77\u5199`);
2146
+ if (p.factor !== void 0 && !(Number.isFinite(p.factor) && p.factor > 0)) problems.push(`${where}\uFF1Afactor \u8981\u662F\u6B63\u6570`);
2147
+ if (p.currency_flag !== void 0 && params[p.currency_flag]?.semantic !== "currency") problems.push(`${where}\uFF1Acurrency_flag ${p.currency_flag} \u4E0D\u662F\u672C\u547D\u4EE4\u7684\u5E01\u79CD\u53C2\u6570`);
2148
+ }
2149
+ function buildSchema(commands, version) {
2150
+ const problems = [];
2151
+ const out2 = { tool: "tiktok-cli", version, global_flags: GLOBAL_FLAGS.map(dash), commands: {} };
2152
+ for (const cmd of commands) {
2153
+ if (!cmd.write) continue;
2154
+ const v = cmd.values;
2155
+ if (!v) {
2156
+ problems.push(`${cmd.name}\uFF1A\u5199\u547D\u4EE4\u6CA1\u6709 values \u6807\u6CE8`);
2157
+ continue;
2158
+ }
2159
+ const params = v.params ?? {};
2160
+ const seen = /* @__PURE__ */ new Map();
2161
+ const claim = (flag, place) => {
2162
+ if (GLOBAL_FLAGS.includes(flag)) problems.push(`${cmd.name}\uFF1A--${flag} \u662F\u5168\u5C40\u53C2\u6570\uFF0C\u4E0D\u5199\u8FDB\u547D\u4EE4\u81EA\u5DF1\u7684\u6E05\u5355`);
2163
+ if (seen.has(flag)) problems.push(`${cmd.name}\uFF1A--${flag} \u540C\u65F6\u5728 ${seen.get(flag)} \u548C ${place}`);
2164
+ seen.set(flag, place);
2165
+ if (!cmd.flags.includes(flag)) problems.push(`${cmd.name}\uFF1A\u6E05\u5355\u91CC\u7684 --${flag} \u4E0D\u662F\u8FD9\u6761\u547D\u4EE4\u8BA4\u7684\u53C2\u6570`);
2166
+ };
2167
+ for (const f of Object.keys(params)) claim(f, "params");
2168
+ if (v.subject) claim(v.subject.flag, "subject");
2169
+ for (const f of v.unchecked ?? []) claim(f, "unchecked");
2170
+ for (const f of v.opaque ?? []) claim(f, "opaque");
2171
+ for (const f of cmd.flags) if (!seen.has(f)) problems.push(`${cmd.name}\uFF1A--${f} \u6CA1\u5199\u8FDB\u6E05\u5355\uFF08\u5E26\u94B1\u7684\u8FDB params\uFF0C\u4E0D\u662F\u94B1\u7684\u8FDB unchecked\uFF09`);
2172
+ for (const f of v.repeatable ?? []) if (!seen.has(f) || seen.get(f) === "opaque") problems.push(`${cmd.name}\uFF1Arepeatable --${f} \u4E0D\u662F\u5B83\u7684\u53C2\u6570`);
2173
+ for (const [f, p] of Object.entries(params)) checkParam(`${cmd.name} --${f}`, p, params, problems);
2174
+ const m = { write: true, params: {} };
2175
+ if (v.subject) m.subject = { flag: dash(v.subject.flag), kind: v.subject.kind };
2176
+ for (const [f, p] of Object.entries(params)) {
2177
+ m.params[dash(f)] = { ...p, ...p.semantic === "currency" ? { type: "string" } : {}, ...p.currency_flag ? { currency_flag: dash(p.currency_flag) } : {} };
2178
+ }
2179
+ if (v.repeatable?.length) m.repeatable = sorted(v.repeatable);
2180
+ if (v.opaque?.length) m.opaque = sorted(v.opaque);
2181
+ if (v.unchecked?.length) m.unchecked = sorted(v.unchecked);
2182
+ out2.commands[cmd.name] = m;
2183
+ }
2184
+ if (problems.length) throw new Error(`\u5199\u547D\u4EE4\u6E05\u5355\u4E0D\u5B8C\u6574\uFF1A
2185
+ - ${problems.join("\n- ")}`);
2186
+ return out2;
2187
+ }
2188
+
2010
2189
  // src/index.ts
2011
2190
  var NO_SHOP_COMMANDS = /* @__PURE__ */ new Set(["auth link", "apps add", "apps list", "apps disable", "apps available", "apps set-shop"]);
2012
2191
  var SHOP_OPTIONAL_BUT_READ = /* @__PURE__ */ new Set(["auth link", "apps set-shop"]);
@@ -2063,11 +2242,13 @@ function normalizeBooleanFlags(args2) {
2063
2242
  function out(obj) {
2064
2243
  process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
2065
2244
  }
2245
+ var SCHEMA = { name: "schema", summary: "\u5199\u547D\u4EE4\u53C2\u6570\u6E05\u5355\uFF08--json\uFF1B\u4E0D\u9700\u8981 --shop\uFF09\uFF1A\u6BCF\u6761\u5199\u547D\u4EE4\u7684\u6BCF\u4E2A\u53C2\u6570\u662F\u4EC0\u4E48\u610F\u601D\u3001\u4EC0\u4E48\u5355\u4F4D\uFF0C\u4F9B Optima COO \u7684\u5199\u5165\u6570\u503C\u5B88\u95E8\u6309\u7248\u672C\u56FA\u5B9A", flags: ["json"], noShop: true };
2066
2246
  function usage() {
2067
2247
  const lines = ["tiktok <domain> <verb> --shop <shopId> [--flags]", "", "\u547D\u4EE4\uFF1A"];
2068
2248
  for (const c of COMMANDS) lines.push(` ${c.name.padEnd(22)} ${c.summary}`);
2069
2249
  lines.push(` ${"shops".padEnd(22)} \u67E5\u5F53\u524D\u767B\u5F55\u7528\u6237\u5DF2\u6388\u6743\u5E97\u94FA`);
2070
2250
  lines.push(` ${"images upload".padEnd(22)} \u4E0A\u4F20\u672C\u5730\u56FE\u7247\uFF08--file --use-case [--out]\uFF09`);
2251
+ lines.push(` ${"schema".padEnd(22)} ${SCHEMA.summary}`);
2071
2252
  lines.push("", "\u6BCF\u6761\u547D\u4EE4\u53EA\u8BA4\u5B83\u81EA\u5DF1\u7684\u53C2\u6570\uFF08`tiktok <\u547D\u4EE4> --help` \u770B\uFF09\uFF0C\u4E0D\u8BA4\u7684\u4F1A\u5F53\u573A\u62A5 VALIDATION \u2014\u2014 \u4E0D\u4F1A\u518D\u88AB\u9759\u9ED8\u4E22\u6389\u3002");
2072
2253
  lines.push("", "\u901A\u7528\uFF1A--shop \u5FC5\u586B\uFF08\u9664 shops / auth link / apps add|list|disable\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");
2073
2254
  process.stderr.write(lines.join("\n") + "\n");
@@ -2106,6 +2287,15 @@ if (positional[0] === "shops") {
2106
2287
  out(await listShops());
2107
2288
  process.exit(0);
2108
2289
  }
2290
+ if (positional[0] === "schema") {
2291
+ if (wantsHelp) {
2292
+ process.stderr.write(commandUsage(SCHEMA));
2293
+ process.exit(0);
2294
+ }
2295
+ rejectUnknownFlags(SCHEMA.name, SCHEMA.flags, positional.slice(1), true);
2296
+ out(buildSchema(COMMANDS, cliVersion()));
2297
+ process.exit(0);
2298
+ }
2109
2299
  if (positional[0] === "images" && positional[1] === "upload") {
2110
2300
  if (wantsHelp) {
2111
2301
  process.stderr.write(commandUsage(IMAGES_UPLOAD));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/tiktok-cli",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "description": "TikTok Shop CLI for Optima agents — 经 tiktok-backend 操作店铺",
5
5
  "type": "module",
6
6
  "bin": {