@petercjl/ecom-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/INSTALL.md +1 -1
- package/README.md +16 -0
- package/bin/ecom-cli.mjs +1 -1
- package/capabilities.json +238 -5
- package/lib/browser.mjs +42 -4
- package/lib/cli.mjs +21 -4
- package/lib/maintenance.mjs +6 -1
- package/lib/sycm-vendor/LICENSE +21 -0
- package/lib/sycm-vendor/business-capabilities.mjs +57 -0
- package/lib/sycm-vendor/errors.mjs +26 -0
- package/lib/sycm-vendor/operation-registry.mjs +196 -0
- package/lib/sycm-vendor/provenance.json +12 -0
- package/lib/sycm.mjs +208 -0
- package/package.json +1 -1
- package/scripts/validate.mjs +12 -0
- package/skills/ecom-cli/SKILL.md +15 -1
- package/skills/ecom-cli/adapters/generic.json +1 -1
- package/skills/ecom-cli/capabilities.json +5 -1
- package/skills/ecom-cli/package.json +1 -1
- package/skills/ecom-cli/references/sycm.md +40 -0
package/INSTALL.md
CHANGED
|
@@ -10,4 +10,4 @@ User prompt: “帮我安装 @petercjl/ecom-cli,并安装它的配套 Skill。
|
|
|
10
10
|
4. Verify `current=true`, `managed=true`, `edited=false`, SHA-256 verification, package version and returned CLI entry. Load the installed `SKILL.md` through the current Agent's own discovery mechanism; refresh discovery if the host requires it. Installation is complete only after both package integrity and host discovery succeed.
|
|
11
11
|
5. Use the returned isolated `.runtime/bin/ecom-cli.mjs` with Node.js for subsequent calls. The bootstrap npm package may then be removed; this Skill and runtime remain functional. A system Node.js runtime and the separately installed Ecom browser are declared external dependencies.
|
|
12
12
|
|
|
13
|
-
Existing unmanaged or edited destinations are preserved and produce a structured conflict. Choose an unused destination or reconcile user edits with a backup. Never install other Agents merely because their directories exist. Runtime support is capability-based; current real browser validation is macOS.
|
|
13
|
+
Existing unmanaged or edited destinations are preserved and produce a structured conflict. Choose an unused destination or reconcile user edits with a backup. Never install other Agents merely because their directories exist. Runtime support is capability-based; current real browser validation is macOS.
|
package/README.md
CHANGED
|
@@ -33,3 +33,19 @@ Task entry checks stable `latest` at most daily, with a 1.5 second bound. Detect
|
|
|
33
33
|
`ecom-cli help --json`, `version --json`, `capabilities --json`, `schema --json`, and `doctor --json` expose supported commands and versioned results. Errors use JSON with stable codes; exit code 2 means invalid arguments, 1 means operational failure. stdout contains results; stderr contains diagnostics. Multiple Ecom instances require explicit `--profile` or `--instance` selection. Connection authenticates through owner-only local mailbox files, verifies the live Ecom executable, and uses no remote debugging port.
|
|
34
34
|
|
|
35
35
|
The browser owns sessions, containers and authorization. This package owns discovery, read-only transport and managed installation. The Skill documents invocation, diagnosis and recovery. Additional browser capabilities will extend this same command/Skill contract.
|
|
36
|
+
|
|
37
|
+
## 业务能力迁移(开发中)
|
|
38
|
+
|
|
39
|
+
内置 sycmcli 0.4.0 的取数目录和 7 类填数计划。135 个取数入口统一使用固定原生目录,覆盖生意参谋、万相台、达摩盘与消费券。原生层在指定持久容器中准备服务认证并发送已登记的 GET/POST 读取,要求浏览器与 CLI 目录哈希一致。目录接入与逐项真实验收分别记录;旧浏览器兼容 88 个生意参谋 GET 入口,其余提示升级。填数支持计划、审查与确认,实际提交和回读由独立执行能力承担。
|
|
40
|
+
|
|
41
|
+
发给当前 Agent:
|
|
42
|
+
|
|
43
|
+
> 使用 Ecom CLI,查看指定账号可以查询哪些生意参谋数据。
|
|
44
|
+
|
|
45
|
+
> 使用 Ecom CLI,获取指定账号昨天的交易概况。
|
|
46
|
+
|
|
47
|
+
> 使用 Ecom CLI,为指定账号生成商品草稿的操作计划,供我审查。
|
|
48
|
+
|
|
49
|
+
开发者通过 `sycm operations`、`sycm schema --operation ID`、`sycm run --operation ID --container VALUE --params-json JSON` 发现和调用原子取数能力;`sycm mutation plan/show/apply` 承接计划与确认。所有命令均支持结构化 JSON。
|
|
50
|
+
|
|
51
|
+
取数优先使用后台原生请求。需要准备平台会话时,当前 Agent 可通过 `sycm open --operation ID --container VALUE` 在该容器打开注册入口,默认后台打开或复用;`--foreground` 用于明确需要前台的登录与验证。登录完成后继续调用原生接口,保留账号已设置的首页。
|
package/bin/ecom-cli.mjs
CHANGED
|
@@ -4,6 +4,6 @@ try {
|
|
|
4
4
|
const result = await main(process.argv.slice(2));
|
|
5
5
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
6
6
|
} catch (error) {
|
|
7
|
-
process.stdout.write(JSON.stringify({ok:false,error:{code:error.code || 'CLI_ERROR',message:error.code ? error.message : 'Operation failed; inspect doctor and operation status',executed:error.executed ?? false,...(error.operationId ? {operationId:error.operationId} : {}),...(error.httpStatus!==undefined?{httpStatus:error.httpStatus}:{}),...(error.recovery ? {recovery:error.recovery} : {})}}) + '\n');
|
|
7
|
+
process.stdout.write(JSON.stringify({ok:false,error:{code:error.code || 'CLI_ERROR',message:error.code ? error.message : 'Operation failed; inspect doctor and operation status',executed:error.executed ?? false,...(error.operationId ? {operationId:error.operationId} : {}),...(error.httpStatus!==undefined?{httpStatus:error.httpStatus}:{}),...(error.diagnostics?{diagnostics:error.diagnostics}:{}),...(error.recovery ? {recovery:error.recovery} : {})}}) + '\n');
|
|
8
8
|
process.exitCode = error.code === 'INVALID_ARGUMENT' ? 2 : 1;
|
|
9
9
|
}
|
package/capabilities.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@petercjl/ecom-cli",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"skills": [
|
|
6
6
|
"ecom-cli"
|
|
7
7
|
],
|
|
@@ -251,6 +251,212 @@
|
|
|
251
251
|
"PROTOCOL_MISMATCH",
|
|
252
252
|
"OPERATION_TIMEOUT"
|
|
253
253
|
]
|
|
254
|
+
},
|
|
255
|
+
"sycm operations": {
|
|
256
|
+
"options": {
|
|
257
|
+
"--json": {
|
|
258
|
+
"type": "boolean"
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
"sideEffects": "none",
|
|
262
|
+
"authorization": "Registered reads only, or local mutation-plan/confirmation records; no automatic platform writes. Credentials stay native."
|
|
263
|
+
},
|
|
264
|
+
"sycm capabilities": {
|
|
265
|
+
"options": {
|
|
266
|
+
"--json": {
|
|
267
|
+
"type": "boolean"
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
"sideEffects": "none",
|
|
271
|
+
"authorization": "Registered reads only, or local mutation-plan/confirmation records; no automatic platform writes. Credentials stay native."
|
|
272
|
+
},
|
|
273
|
+
"sycm schema": {
|
|
274
|
+
"options": {
|
|
275
|
+
"--json": {
|
|
276
|
+
"type": "boolean"
|
|
277
|
+
},
|
|
278
|
+
"--operation": {
|
|
279
|
+
"type": "string",
|
|
280
|
+
"required": true
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
"sideEffects": "none",
|
|
284
|
+
"authorization": "Registered reads only, or local mutation-plan/confirmation records; no automatic platform writes. Credentials stay native."
|
|
285
|
+
},
|
|
286
|
+
"sycm run": {
|
|
287
|
+
"options": {
|
|
288
|
+
"--json": {
|
|
289
|
+
"type": "boolean"
|
|
290
|
+
},
|
|
291
|
+
"--profile": {
|
|
292
|
+
"type": "string"
|
|
293
|
+
},
|
|
294
|
+
"--instance": {
|
|
295
|
+
"type": "string"
|
|
296
|
+
},
|
|
297
|
+
"--container": {
|
|
298
|
+
"type": "string",
|
|
299
|
+
"required": true
|
|
300
|
+
},
|
|
301
|
+
"--operation": {
|
|
302
|
+
"type": "string",
|
|
303
|
+
"required": true
|
|
304
|
+
},
|
|
305
|
+
"--params-json": {
|
|
306
|
+
"type": "string"
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
"sideEffects": "local-write",
|
|
310
|
+
"authorization": "Registered reads only, or local mutation-plan/confirmation records; no automatic platform writes. Credentials stay native.",
|
|
311
|
+
"output": {
|
|
312
|
+
"operation": "registered ID",
|
|
313
|
+
"service": "sycm, sycm-trade, alimama, dmp or coupon",
|
|
314
|
+
"instance": "pinned native instance",
|
|
315
|
+
"container": "stable id and current label",
|
|
316
|
+
"httpStatus": 200,
|
|
317
|
+
"businessStatus": "success after business-envelope checks",
|
|
318
|
+
"data": "native-redacted source response",
|
|
319
|
+
"registryHash": "compiled browser/CLI catalog identity"
|
|
320
|
+
},
|
|
321
|
+
"limits": {
|
|
322
|
+
"paramsBytes": 24576,
|
|
323
|
+
"nativeResponseBytes": 2097152,
|
|
324
|
+
"nativeOperationTimeoutMs": 50000,
|
|
325
|
+
"clientTimeoutMs": 75000,
|
|
326
|
+
"automaticRetries": false,
|
|
327
|
+
"methods": "Fixed catalog GET/POST reads; arbitrary method/path/body forbidden",
|
|
328
|
+
"accountBinding": "Current Ecom persistent container, no separate CLI store configuration"
|
|
329
|
+
},
|
|
330
|
+
"errors": [
|
|
331
|
+
"BROWSER_UPGRADE_REQUIRED",
|
|
332
|
+
"AUTH_CONTEXT_REQUIRED",
|
|
333
|
+
"AUTH_REQUIRED",
|
|
334
|
+
"AUTH_OR_RISK_CHALLENGE",
|
|
335
|
+
"PERMISSION_REQUIRED",
|
|
336
|
+
"RISK_VERIFICATION_REQUIRED",
|
|
337
|
+
"SHOP_VERIFICATION_FAILED",
|
|
338
|
+
"BUSINESS_READ_TIMEOUT",
|
|
339
|
+
"BUSINESS_RESPONSE_INVALID",
|
|
340
|
+
"PROTOCOL_MISMATCH"
|
|
341
|
+
],
|
|
342
|
+
"errorDiagnostics": {
|
|
343
|
+
"phase": "bounded native stage",
|
|
344
|
+
"netError": "Chromium network code",
|
|
345
|
+
"bodyKind": "json/html/text/none",
|
|
346
|
+
"matchingTabFound": "boolean",
|
|
347
|
+
"entryCookieCount": "count only; does not prove authentication",
|
|
348
|
+
"failureClass": "safe fixed classification",
|
|
349
|
+
"publicScripts": "optional sanitized public CDN scripts; credentials excluded"
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
"sycm mutation plan": {
|
|
353
|
+
"options": {
|
|
354
|
+
"--json": {
|
|
355
|
+
"type": "boolean"
|
|
356
|
+
},
|
|
357
|
+
"--profile": {
|
|
358
|
+
"type": "string"
|
|
359
|
+
},
|
|
360
|
+
"--instance": {
|
|
361
|
+
"type": "string"
|
|
362
|
+
},
|
|
363
|
+
"--container": {
|
|
364
|
+
"type": "string",
|
|
365
|
+
"required": true
|
|
366
|
+
},
|
|
367
|
+
"--operation": {
|
|
368
|
+
"type": "string",
|
|
369
|
+
"required": true
|
|
370
|
+
},
|
|
371
|
+
"--params-json": {
|
|
372
|
+
"type": "string",
|
|
373
|
+
"required": true
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
"sideEffects": "local-write",
|
|
377
|
+
"authorization": "Registered reads only, or local mutation-plan/confirmation records; no automatic platform writes. Credentials stay native."
|
|
378
|
+
},
|
|
379
|
+
"sycm mutation show": {
|
|
380
|
+
"options": {
|
|
381
|
+
"--json": {
|
|
382
|
+
"type": "boolean"
|
|
383
|
+
},
|
|
384
|
+
"--plan": {
|
|
385
|
+
"type": "string",
|
|
386
|
+
"required": true
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
"sideEffects": "none",
|
|
390
|
+
"authorization": "Registered reads only, or local mutation-plan/confirmation records; no automatic platform writes. Credentials stay native."
|
|
391
|
+
},
|
|
392
|
+
"sycm mutation apply": {
|
|
393
|
+
"options": {
|
|
394
|
+
"--json": {
|
|
395
|
+
"type": "boolean"
|
|
396
|
+
},
|
|
397
|
+
"--profile": {
|
|
398
|
+
"type": "string"
|
|
399
|
+
},
|
|
400
|
+
"--instance": {
|
|
401
|
+
"type": "string"
|
|
402
|
+
},
|
|
403
|
+
"--container": {
|
|
404
|
+
"type": "string",
|
|
405
|
+
"required": true
|
|
406
|
+
},
|
|
407
|
+
"--plan": {
|
|
408
|
+
"type": "string",
|
|
409
|
+
"required": true
|
|
410
|
+
},
|
|
411
|
+
"--confirm": {
|
|
412
|
+
"type": "string",
|
|
413
|
+
"required": true
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
"sideEffects": "local-write",
|
|
417
|
+
"authorization": "Registered reads only, or local mutation-plan/confirmation records; no automatic platform writes. Credentials stay native."
|
|
418
|
+
},
|
|
419
|
+
"sycm open": {
|
|
420
|
+
"options": {
|
|
421
|
+
"--json": {
|
|
422
|
+
"type": "boolean"
|
|
423
|
+
},
|
|
424
|
+
"--profile": {
|
|
425
|
+
"type": "string"
|
|
426
|
+
},
|
|
427
|
+
"--instance": {
|
|
428
|
+
"type": "string"
|
|
429
|
+
},
|
|
430
|
+
"--container": {
|
|
431
|
+
"type": "string",
|
|
432
|
+
"required": true
|
|
433
|
+
},
|
|
434
|
+
"--operation": {
|
|
435
|
+
"type": "string",
|
|
436
|
+
"required": true
|
|
437
|
+
},
|
|
438
|
+
"--foreground": {
|
|
439
|
+
"type": "boolean",
|
|
440
|
+
"default": false
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
"sideEffects": "local-write",
|
|
444
|
+
"authorization": "Explicit page navigation or an authorized recovery fallback; fixed registered entry in the selected container. Background/reuse by default, foreground only when selected.",
|
|
445
|
+
"output": {
|
|
446
|
+
"navigation": "started or reused, not login success",
|
|
447
|
+
"container": "stable ID and current name",
|
|
448
|
+
"requestedUrl": "compiled registered entry",
|
|
449
|
+
"background": "boolean",
|
|
450
|
+
"authentication": "not-verified"
|
|
451
|
+
},
|
|
452
|
+
"errors": [
|
|
453
|
+
"BROWSER_UPGRADE_REQUIRED",
|
|
454
|
+
"BUSINESS_PAGE_UNSUPPORTED",
|
|
455
|
+
"CONTAINER_NOT_FOUND",
|
|
456
|
+
"CONTAINER_AMBIGUOUS",
|
|
457
|
+
"PROTOCOL_MISMATCH"
|
|
458
|
+
],
|
|
459
|
+
"sideEffectDetails": "Registered browser navigation in the selected container; no website submission."
|
|
254
460
|
}
|
|
255
461
|
},
|
|
256
462
|
"transport": {
|
|
@@ -409,7 +615,7 @@
|
|
|
409
615
|
"enabledByDefault": true,
|
|
410
616
|
"intervalHours": 24,
|
|
411
617
|
"timeoutMs": 1500,
|
|
412
|
-
"trigger": "containers list, containers inspect and session request task entry",
|
|
618
|
+
"trigger": "containers list, containers inspect and session request task entry, sycm run, sycm mutation plan/apply, sycm open",
|
|
413
619
|
"configuration": "Stable latest channel; detection only. Explicit update activates only the selected target runtime and Skill together.",
|
|
414
620
|
"disable": "ECOM_CLI_UPDATE_CHECK=0",
|
|
415
621
|
"recovery": "Offline checks retain installed runtime; local edit/ownership conflicts stop replacement; prior target kept as backup. Use recorded candidate and inspect target status before retrying.",
|
|
@@ -417,9 +623,9 @@
|
|
|
417
623
|
},
|
|
418
624
|
"compatibility": {
|
|
419
625
|
"agents": "Any Agent with terminal, Node.js and Skill discovery capabilities; generic adapter",
|
|
420
|
-
"macos": "
|
|
626
|
+
"macos": "Native container list, account inspection and authorized session GET verified on macOS arm64 Ecom Dev; host discovery is checked separately",
|
|
421
627
|
"windows": "Package mechanics only; browser transport unsupported",
|
|
422
|
-
"historicalUpdater": "
|
|
628
|
+
"historicalUpdater": "Actual published 0.1.1, 0.1.2, 0.1.3 and 0.2.0 updater entries verified with real npm, preserved prior versions, isolated targets and failure recovery"
|
|
423
629
|
},
|
|
424
630
|
"errors": [
|
|
425
631
|
"INSTANCE_NOT_RUNNING",
|
|
@@ -444,5 +650,32 @@
|
|
|
444
650
|
"BUSY",
|
|
445
651
|
"REQUEST_FAILED",
|
|
446
652
|
"NON_JSON_RESPONSE"
|
|
447
|
-
]
|
|
653
|
+
],
|
|
654
|
+
"businessIntegration": {
|
|
655
|
+
"sourcePackage": "@petercjl/sycmcli",
|
|
656
|
+
"sourceVersion": "0.4.0",
|
|
657
|
+
"registeredReads": 135,
|
|
658
|
+
"nativeGetCandidates": 88,
|
|
659
|
+
"serviceAdaptersRequired": 0,
|
|
660
|
+
"mutationWorkflows": 7,
|
|
661
|
+
"automaticMutation": false,
|
|
662
|
+
"inventoryCommand": "sycm capabilities",
|
|
663
|
+
"schemaCommand": "sycm schema --operation ID",
|
|
664
|
+
"readCommand": "sycm run --operation ID --container VALUE --params-json JSON",
|
|
665
|
+
"verification": "Transport availability is separate from per-endpoint live acceptance; inspect each operation schema. Plans authorize a workflow, not an executed submission.",
|
|
666
|
+
"sourceRegistryReads": 124,
|
|
667
|
+
"sourceFirstClassReadEndpoints": 11,
|
|
668
|
+
"nativeBusinessReadDefinitions": 135,
|
|
669
|
+
"serviceRuntimeAcceptance": "pending native build and per-service live checks",
|
|
670
|
+
"requiredNativeProtocol": 1,
|
|
671
|
+
"registryHashAgreementRequired": true,
|
|
672
|
+
"containerBinding": "Stable Ecom container ID, browser-owned session and native account verification; no separate Chrome store profile",
|
|
673
|
+
"nativeRegistryHash": "26d302541c1641b00ce45c6df4ae32bbb90e2b21352fe5f91e3cfedb21e35d5e",
|
|
674
|
+
"pageFallback": {
|
|
675
|
+
"command": "sycm open --operation ID --container VALUE",
|
|
676
|
+
"requiredNativeCapability": "businessPageOpenProtocol=1",
|
|
677
|
+
"default": "background and reuse",
|
|
678
|
+
"strategy": "Native HTTP and private auth initialization first. Open the registered container entry only when needed and authorized; verify data again afterward."
|
|
679
|
+
}
|
|
680
|
+
}
|
|
448
681
|
}
|
package/lib/browser.mjs
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import crypto from 'node:crypto';
|
|
4
5
|
import {execFileSync} from 'node:child_process';
|
|
5
6
|
import {setTimeout as pause} from 'node:timers/promises';
|
|
6
7
|
import {fail,privatePath,writeNew,exists} from './common.mjs';
|
|
7
8
|
const ps=(args)=>execFileSync('/bin/ps',args,{encoding:'utf8',timeout:3000,maxBuffer:4*1024*1024});
|
|
9
|
+
export function processProfile(executable,command) {
|
|
10
|
+
const explicit=command.match(/(?:^| )--user-data-dir=(.*?)(?= --|$)/)?.[1];
|
|
11
|
+
if(explicit)return explicit;
|
|
12
|
+
if(executable.endsWith('/Ecom.app/Contents/MacOS/Ecom'))return path.join(os.homedir(),'Library','Application Support','Ecom','Ecom');
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
8
15
|
export function discover({profile,instance}={}) {
|
|
9
16
|
if(process.platform!=='darwin') throw fail('PLATFORM_UNSUPPORTED','Browser transport currently supports macOS; no debugging fallback is available');
|
|
10
17
|
const candidates=[];
|
|
@@ -13,7 +20,7 @@ export function discover({profile,instance}={}) {
|
|
|
13
20
|
if(!m || Number(m[2])!==process.getuid() || !/\/(Ecom Dev|Ecom)\.app\/Contents\/MacOS\/\1$/.test(m[3])) continue;
|
|
14
21
|
let command; try {command=ps(['-ww','-p',m[1],'-o','command=']).trim();}catch{continue;}
|
|
15
22
|
if(/--(?:remote-debugging|headless|enable-automation)/.test(command)) continue;
|
|
16
|
-
const selected=
|
|
23
|
+
const selected=processProfile(m[3],command);
|
|
17
24
|
if(!selected || !path.isAbsolute(selected) || (profile && path.resolve(profile)!==path.resolve(selected))) continue;
|
|
18
25
|
const folder=path.join(selected,'Default','.ecom-native-cli'), file=path.join(folder,'endpoint.json');
|
|
19
26
|
if(!exists(file)) continue;
|
|
@@ -27,7 +34,7 @@ export function discover({profile,instance}={}) {
|
|
|
27
34
|
return candidates[0];
|
|
28
35
|
}
|
|
29
36
|
const instanceOf=({endpoint,channel})=>({id:endpoint.instanceId,pid:endpoint.pid,channel});
|
|
30
|
-
const credentialKey=/token|cookie|csrf|password|authorization|secret|sessionid|api[-_]?key|access[-_]?key|credential
|
|
37
|
+
const credentialKey=/token|cookie|csrf|password|authorization|secret|sessionid|api[-_]?key|access[-_]?key|credential|^__d?tk$|^session$|^loginpointid$/i;
|
|
31
38
|
function homeUrl(value) {
|
|
32
39
|
try {
|
|
33
40
|
const url=new URL(value);if(url.protocol!=='https:')return null;
|
|
@@ -83,6 +90,36 @@ export async function requestInConnection(connection,options) {
|
|
|
83
90
|
if(body.containerId!==container.id||body.httpStatus!==200||!Object.hasOwn(body,'data'))throw fail('PROTOCOL_MISMATCH','Session response does not match selected container or JSON response contract',{operationId:body.operationId,executed:'unknown'});
|
|
84
91
|
return {ok:true,schemaVersion:1,operationId:body.operationId,instance:instanceOf(connection),container,request:{method:'GET',url},httpStatus:body.httpStatus,data:body.data,businessStatus:'not-evaluated'};
|
|
85
92
|
}
|
|
93
|
+
export async function registeredReadInConnection(connection,options,operation,params,registryHash) {
|
|
94
|
+
if(connection.endpoint.businessReadProtocol!==1)throw fail('BROWSER_UPGRADE_REQUIRED','This operation needs an Ecom build with native business read protocol 1');
|
|
95
|
+
if(connection.endpoint.businessReadRegistryHash!==registryHash)throw fail('BROWSER_UPGRADE_REQUIRED','Browser and CLI business catalogs differ; update them together');
|
|
96
|
+
const listed=await readMailbox(connection),container=resolveContainer(listed.containers,options.container);
|
|
97
|
+
const body=await callMailbox(connection,{method:'business.read',containerId:container.id,operation,params},75000);
|
|
98
|
+
if(body.containerId!==container.id||body.businessOperation!==operation||body.httpStatus!==200||!Object.hasOwn(body,'data'))throw fail('PROTOCOL_MISMATCH','Registered response differs from the pinned container or operation',{operationId:body.operationId,executed:'unknown'});
|
|
99
|
+
return {ok:true,schemaVersion:1,operationId:body.operationId,instance:instanceOf(connection),container,operation,httpStatus:body.httpStatus,data:body.data,...(Array.isArray(body.dataFlows)&&body.dataFlows.length?{dataFlows:body.dataFlows}:{}),transport:'native-business-read',identityBinding:'container-bound-account',registryHash};
|
|
100
|
+
}
|
|
101
|
+
export async function openBusinessPageInConnection(connection,options,operation,entryUrl,registryHash) {
|
|
102
|
+
if(connection.endpoint.businessPageOpenProtocol!==1||connection.endpoint.businessReadRegistryHash!==registryHash)throw fail('BROWSER_UPGRADE_REQUIRED','Update Ecom to the matching registered-page navigation capability');
|
|
103
|
+
const listed=await readMailbox(connection),container=resolveContainer(listed.containers,options.container);
|
|
104
|
+
const background=!options.foreground;
|
|
105
|
+
const body=await callMailbox(connection,{method:'shops.open',containerId:container.id,businessOperation:operation,background,reuse:background});
|
|
106
|
+
if(body.containerId!==container.id||!['started','reused'].includes(body.navigation)||body.requestedUrl!==entryUrl)throw fail('PROTOCOL_MISMATCH','Navigation target differs from the registered container or entry page',{operationId:body.operationId,executed:'unknown'});
|
|
107
|
+
return {ok:true,schemaVersion:1,operationId:body.operationId,instance:instanceOf(connection),container,operation,requestedUrl:body.requestedUrl,navigation:body.navigation,background,authentication:'not-verified'};
|
|
108
|
+
}
|
|
109
|
+
function safeDiagnostics(value) {
|
|
110
|
+
if(!value||typeof value!=='object')return undefined;
|
|
111
|
+
const result={};
|
|
112
|
+
if(typeof value.phase==='string'&&/^[a-z-]{1,32}$/.test(value.phase))result.phase=value.phase;
|
|
113
|
+
if(Number.isInteger(value.netError)&&value.netError<=0&&value.netError>=-9999)result.netError=value.netError;
|
|
114
|
+
if(['json','html','text','none'].includes(value.bodyKind))result.bodyKind=value.bodyKind;
|
|
115
|
+
if(typeof value.matchingTabFound==='boolean')result.matchingTabFound=value.matchingTabFound;
|
|
116
|
+
if(Number.isInteger(value.entryCookieCount)&&value.entryCookieCount>=0&&value.entryCookieCount<=100000)result.entryCookieCount=value.entryCookieCount;
|
|
117
|
+
if(['unclassified','cors-rejected','csrf-rejected','verification-required','forbidden'].includes(value.failureClass))result.failureClass=value.failureClass;
|
|
118
|
+
if(Array.isArray(value.publicScripts))result.publicScripts=value.publicScripts.slice(0,20).filter(url=>{
|
|
119
|
+
try{const item=new URL(url);return item.protocol==='https:'&&item.hostname==='g.alicdn.com'&&!item.username&&!item.password&&!item.search&&!item.hash&&item.pathname.endsWith('.js')&&url.length<2048;}catch{return false;}
|
|
120
|
+
});
|
|
121
|
+
return Object.keys(result).length?result:undefined;
|
|
122
|
+
}
|
|
86
123
|
export async function callMailbox({folder,endpoint},payload,timeoutMs=10000) {
|
|
87
124
|
privatePath(folder,true);privatePath(path.join(folder,'endpoint.json'));
|
|
88
125
|
const current=JSON.parse(fs.readFileSync(path.join(folder,'endpoint.json'),'utf8'));
|
|
@@ -96,13 +133,14 @@ export async function callMailbox({folder,endpoint},payload,timeoutMs=10000) {
|
|
|
96
133
|
if(exists(response)) {
|
|
97
134
|
privatePath(response);
|
|
98
135
|
const raw=fs.readFileSync(response,'utf8');
|
|
99
|
-
if(Buffer.byteLength(raw)>(
|
|
136
|
+
if(Buffer.byteLength(raw)>(['session.get','business.read'].includes(payload.method)?2*1024*1024+65536:1024*1024)) throw fail('PROTOCOL_MISMATCH','Oversized response',{operationId:id,executed:'unknown'});
|
|
100
137
|
let body;try{body=JSON.parse(raw);}catch{await pause(25);continue;}
|
|
101
138
|
if(body.schemaVersion!==1 || body.instanceId!==endpoint.instanceId || body.operationId!==id) throw fail('PROTOCOL_MISMATCH','Response does not match this operation',{operationId:id,executed:'unknown'});
|
|
102
139
|
fs.unlinkSync(response);
|
|
103
140
|
if(body.ok!==true) {
|
|
104
141
|
const code=/^[A-Z][A-Z0-9_]{0,63}$/.test(body.error?.code??'')?body.error.code:'BROWSER_ERROR';
|
|
105
|
-
|
|
142
|
+
const diagnostics=safeDiagnostics(body.error?.diagnostics);
|
|
143
|
+
throw fail(code,'Ecom refused the request: '+code,{operationId:id,executed:body.error?.executed??'unknown',...(Number.isInteger(body.error?.httpStatus)?{httpStatus:body.error.httpStatus}:{}),...(diagnostics?{diagnostics}:{})});
|
|
106
144
|
}
|
|
107
145
|
return body;
|
|
108
146
|
}
|
package/lib/cli.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import {root,pkg,fail} from './common.mjs';
|
|
4
4
|
import {discover,listContainers,inspectContainer,requestSession,validateReadUrl} from './browser.mjs';
|
|
5
5
|
import {source,status,installSkill,checkUpdate,updateInstall} from './maintenance.mjs';
|
|
6
|
+
import {inventory,operationSchema,runRead,openOperation,planMutation,showPlan,authorizePlan,nativeRegistryHash} from './sycm.mjs';
|
|
6
7
|
export const capabilities=()=>JSON.parse(fs.readFileSync(path.join(root,'capabilities.json'),'utf8'));
|
|
7
8
|
export function parse(argv) {
|
|
8
9
|
const positions=[],options={}, caps=capabilities();
|
|
@@ -12,8 +13,8 @@ export function parse(argv) {
|
|
|
12
13
|
if(word==='--version'&&argv.length===1){positions.push('version');continue;}
|
|
13
14
|
const key=word.slice(2);
|
|
14
15
|
if(Object.hasOwn(options,key)) throw fail('INVALID_ARGUMENT','Duplicate option '+word);
|
|
15
|
-
if(['json','help','yes'].includes(key))options[key]=true;
|
|
16
|
-
else if(['agent','target','profile','instance','name','version','container','url','method'].includes(key)&&argv[i+1]&&!argv[i+1].startsWith('--')) options[key]=argv[++i];
|
|
16
|
+
if(['json','help','yes','foreground'].includes(key))options[key]=true;
|
|
17
|
+
else if(['agent','target','profile','instance','name','version','container','url','method','operation','params-json','plan','confirm'].includes(key)&&argv[i+1]&&!argv[i+1].startsWith('--')) options[key]=argv[++i];
|
|
17
18
|
else throw fail('INVALID_ARGUMENT','Unknown option or missing value: '+word);
|
|
18
19
|
}
|
|
19
20
|
let command=positions.join(' ')||'help';
|
|
@@ -27,11 +28,15 @@ export function parse(argv) {
|
|
|
27
28
|
if(!options.container||!options.url)throw fail('INVALID_ARGUMENT','session request requires --container and --url');
|
|
28
29
|
options.url=validateReadUrl(options.url,options.method??'GET');
|
|
29
30
|
}
|
|
31
|
+
if(['sycm open','sycm run','sycm mutation plan','sycm mutation apply'].includes(command)&&!options.container)throw fail('INVALID_ARGUMENT','Specify --container');
|
|
32
|
+
if(['sycm open','sycm schema','sycm run','sycm mutation plan'].includes(command)&&!options.operation)throw fail('INVALID_ARGUMENT','Specify --operation');
|
|
33
|
+
if(['sycm mutation show','sycm mutation apply'].includes(command)&&!options.plan)throw fail('INVALID_ARGUMENT','Specify --plan');
|
|
34
|
+
if(command==='sycm mutation apply'&&!options.confirm)throw fail('INVALID_ARGUMENT','Specify --confirm from the reviewed plan');
|
|
30
35
|
return {command,options,requested};
|
|
31
36
|
}
|
|
32
37
|
export async function main(argv) {
|
|
33
38
|
const {command,options,requested}=parse(argv);
|
|
34
|
-
if(command==='help')return {ok:true,package:pkg.name,version:pkg.version,usage:'ecom-cli containers list|inspect | session request | skill source|status|install|update | update check|install | doctor; use help --json for required options',...capabilities(),...(capabilities().commandContracts[requested]??{})};
|
|
39
|
+
if(command==='help')return {ok:true,package:pkg.name,version:pkg.version,usage:'ecom-cli containers list|inspect | session request | sycm operations|schema|run|open|capabilities|mutation plan|show|apply | skill source|status|install|update | update check|install | doctor; use help --json for required options',...capabilities(),...(capabilities().commandContracts[requested]??{})};
|
|
35
40
|
if(command==='version')return {ok:true,package:pkg.name,version:pkg.version,protocolVersion:1};
|
|
36
41
|
if(command==='capabilities'||command==='schema')return {ok:true,...capabilities()};
|
|
37
42
|
if(command==='skill source') {const item=source();return {...capabilities(),...item,complete:true,items:[item]};}
|
|
@@ -39,8 +44,20 @@ export async function main(argv) {
|
|
|
39
44
|
if(command==='skill install'||command==='skill update')return installSkill(options);
|
|
40
45
|
if(command==='update check')return checkUpdate();
|
|
41
46
|
if(command==='update install')return updateInstall(options);
|
|
47
|
+
if(command==='sycm capabilities')return inventory();
|
|
48
|
+
if(command==='sycm operations'){const result=inventory();return {ok:true,schemaVersion:1,source:result.source,count:result.operations.length,operations:result.operations};}
|
|
49
|
+
if(command==='sycm schema')return {ok:true,schemaVersion:1,operation:operationSchema(options.operation)};
|
|
50
|
+
if(command==='sycm mutation show')return showPlan(options.plan);
|
|
51
|
+
if(['sycm open','sycm run','sycm mutation plan','sycm mutation apply'].includes(command)) {
|
|
52
|
+
const update=await checkUpdate({automatic:true});
|
|
53
|
+
if(update.available)process.stderr.write('Ecom CLI update available: '+update.latest+'\n');
|
|
54
|
+
if(command==='sycm run')return runRead(options);
|
|
55
|
+
if(command==='sycm open')return openOperation(options);
|
|
56
|
+
if(command==='sycm mutation plan')return planMutation(options);
|
|
57
|
+
return authorizePlan(options);
|
|
58
|
+
}
|
|
42
59
|
if(command==='doctor') {
|
|
43
|
-
let instance,error;try{const found=discover(options);instance={id:found.endpoint.instanceId,pid:found.endpoint.pid,channel:found.channel};}catch(e){error={code:e.code,message:e.message};}
|
|
60
|
+
let instance,error;try{const found=discover(options);instance={id:found.endpoint.instanceId,pid:found.endpoint.pid,channel:found.channel,businessReadProtocol:found.endpoint.businessReadProtocol??0,businessPageOpenProtocol:found.endpoint.businessPageOpenProtocol??0,businessRegistryMatches:found.endpoint.businessReadRegistryHash===nativeRegistryHash()};}catch(e){error={code:e.code,message:e.message};}
|
|
44
61
|
return {ok:!error,package:pkg.name,version:pkg.version,node:process.version,platform:process.platform,transport:'private-local-mailbox',debuggingRequired:false,instance,...(error?{error}:{}),skillSource:source().path};
|
|
45
62
|
}
|
|
46
63
|
if(['containers list','containers inspect','session request'].includes(command)) {
|
package/lib/maintenance.mjs
CHANGED
|
@@ -92,6 +92,11 @@ export function installSkill(options,base=root) {
|
|
|
92
92
|
} finally {fs.closeSync(fd);fs.unlinkSync(lock);}
|
|
93
93
|
}
|
|
94
94
|
const cacheRoot=()=>path.join(os.homedir(),'.cache','ecom-cli');
|
|
95
|
+
export function newerStableVersion(candidate,current) {
|
|
96
|
+
const a=candidate.split('.').map(Number),b=current.split('-')[0].split('.').map(Number);
|
|
97
|
+
for(let i=0;i<3;i++){if(a[i]!==b[i])return a[i]>b[i];}
|
|
98
|
+
return current.includes('-');
|
|
99
|
+
}
|
|
95
100
|
export async function checkUpdate({automatic=false}={}) {
|
|
96
101
|
if(automatic&&process.env.ECOM_CLI_UPDATE_CHECK==='0') return {ok:true,disabled:true};
|
|
97
102
|
const cache=path.join(cacheRoot(),'update.json');
|
|
@@ -102,7 +107,7 @@ export async function checkUpdate({automatic=false}={}) {
|
|
|
102
107
|
const response=await fetch('https://registry.npmjs.org/@petercjl%2fecom-cli/latest',{signal:AbortSignal.timeout(1500)});
|
|
103
108
|
if(!response.ok) throw Error('registry');
|
|
104
109
|
const data=await response.json();if(data.name!==pkg.name||!/^\d+\.\d+\.\d+$/.test(data.version))throw Error('shape');
|
|
105
|
-
result={ok:true,version:pkg.version,latest:data.version,available:data.version
|
|
110
|
+
result={ok:true,version:pkg.version,latest:data.version,available:newerStableVersion(data.version,pkg.version),checkedAt:Date.now(),channel:'latest'};
|
|
106
111
|
}catch{result={ok:false,code:'REGISTRY_UNAVAILABLE',version:pkg.version,checkedAt:Date.now(),retained:true};}
|
|
107
112
|
if(automatic) {
|
|
108
113
|
try {fs.mkdirSync(cacheRoot(),{recursive:true,mode:0o700});if(!exists(cache))writeNew(cache,result);else {const archive=cache+'.previous-'+crypto.randomUUID();fs.renameSync(cache,archive);writeNew(cache,result);}}catch{/* Detection must not block read-only work. */}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Peter Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const rows = [
|
|
2
|
+
['alimama-insight', 'alimama', 'fetch-analysis', '行业、竞争、流量与趋势洞察', ['categoryOrKeyword', 'dateRange'], ['insightData']],
|
|
3
|
+
['alimama-onebp-campaign-data', 'alimama', 'fetch', '万相台活动、报表、预测与预算建议', ['dateRange'], ['campaigns', 'report', 'predictions']],
|
|
4
|
+
['category-best-selling-items-analysis', 'category', 'analysis', '类目热销商品及特征分析', ['category', 'dateRange'], ['featureSummary']],
|
|
5
|
+
['category-info-query', 'category', 'fetch', '类目搜索、层级与名称查询', ['categoryIdOrKeyword'], ['categories']],
|
|
6
|
+
['category-keyword-analysis', 'category', 'analysis', '类目关键词规模、效率与机会分析', ['category', 'dateRange'], ['keywordAnalysis']],
|
|
7
|
+
['category-market-analysis', 'category', 'analysis', '品牌、价格带、卖点与竞争格局分析', ['topItems', 'priceSegments'], ['marketStructure']],
|
|
8
|
+
['category-market-insight', 'category', 'workflow', '类目市场洞察完整工作流', ['category', 'dateRange'], ['report', 'detailData']],
|
|
9
|
+
['category-price-band-analysis', 'category', 'analysis', '类目价格带分布与机会分析', ['category', 'dateRange'], ['priceBandAnalysis']],
|
|
10
|
+
['category-top-items-fetch', 'category', 'fetch', '类目榜单商品分价格带取数', ['category', 'dateRange'], ['topItems']],
|
|
11
|
+
['category-top-items-profile', 'category', 'analysis', '类目头部商品画像', ['category', 'dateRange'], ['itemProfile']],
|
|
12
|
+
['category-top-shops-profile', 'category', 'analysis', '类目头部店铺画像', ['category', 'dateRange'], ['shopProfile']],
|
|
13
|
+
['competitor-comparison-action-tracking', 'competitor', 'workflow', '竞店竞品对比与运营动作跟踪', ['competitors', 'dateRange'], ['comparison', 'actions']],
|
|
14
|
+
['competitor-shop-analysis', 'competitor', 'analysis', '竞店核心指标、流量与商品对比', ['shopIds', 'dateRange'], ['shopComparison']],
|
|
15
|
+
['comprehensive-product-data-analysis', 'item', 'workflow', '单品销售、流量、转化、客群综合诊断', ['itemId', 'dateRange'], ['itemDiagnosis']],
|
|
16
|
+
['daily-shop-inspection', 'shop', 'workflow', '店铺日常巡检与风险建议', ['date'], ['inspectionReport']],
|
|
17
|
+
['dmp-shop-insight', 'dmp', 'fetch-analysis', '达摩盘全店货品经营洞察', ['dateRange'], ['shopInsight']],
|
|
18
|
+
['item-gallery-analysis', 'item', 'analysis', '商品主图表现与结构分析', ['itemId', 'dateRange'], ['galleryAnalysis']],
|
|
19
|
+
['item-ops-event-impact-analysis', 'item', 'analysis', '运营动作前后影响分析', ['itemId', 'eventDate'], ['impactAnalysis']],
|
|
20
|
+
['item-promotion-analysis', 'promotion', 'analysis', '商品付费推广效果分析', ['itemId', 'dateRange'], ['promotionAnalysis']],
|
|
21
|
+
['item-search-word-fetch', 'item', 'fetch', '商品全量搜索词与核心词取数', ['itemId', 'dateRange'], ['searchWords']],
|
|
22
|
+
['item-traffic-analysis', 'item', 'analysis', '商品渠道流量结构分析', ['itemId', 'dateRange'], ['trafficAnalysis']],
|
|
23
|
+
['item-traffic-fluctuation-analysis', 'item', 'analysis', '商品流量异动定位', ['itemId', 'dateRange'], ['fluctuationAnalysis']],
|
|
24
|
+
['market-blue-ocean-opportunity-mining', 'market', 'analysis', '蓝海类目与关键词机会发现', ['categoryScope', 'dateRange'], ['opportunities']],
|
|
25
|
+
['marketing-price-check-pro', 'promotion', 'workflow', '本地价盘与线上价格批量核对', ['priceSheet', 'date'], ['priceRiskReport']],
|
|
26
|
+
['mkt_insight-consumer-couponfee', 'promotion', 'fetch', '消费券费用、订单与结算明细查询', ['dateRange'], ['couponFees', 'orders']],
|
|
27
|
+
['mkt_insight-marketing-knowledge', 'promotion', 'knowledge', '营销与价格规则知识问答', ['question'], ['answer']],
|
|
28
|
+
['shop-core-index-analysis', 'shop', 'analysis', '店铺销售、流量与转化核心指标分析', ['dateRange'], ['coreIndexAnalysis']],
|
|
29
|
+
['shop-core-potential-item-analysis', 'shop', 'analysis', '核心品、潜力品与引流款识别', ['dateRange'], ['itemPortfolio']],
|
|
30
|
+
['shop-inspection-problem-info', 'shop', 'fetch', '店铺巡检预警与异常项查询', ['date'], ['problems']],
|
|
31
|
+
['sycm-category-query', 'category', 'fetch', '批量类目 ID 转名称', ['categoryIds'], ['categoryNames']],
|
|
32
|
+
['sycm-flow-itemsource-helper', 'item', 'fetch', '商品流量来源、趋势与下钻查询', ['itemId', 'dateRange'], ['trafficSources']],
|
|
33
|
+
['sycm-item-competitor-analysis', 'competitor', 'fetch-analysis', '本品与竞品指标、趋势、来源、搜索词对比', ['itemId', 'competitorItemIds', 'dateRange'], ['competitorData']],
|
|
34
|
+
['sycm-item-data-fetch', 'item', 'fetch', '商品排行、销售、退款、价格、标题、详情、流量和客群取数', ['itemIdOrScope', 'dateRange'], ['itemData']],
|
|
35
|
+
['sycm-item-rank-helper', 'item', 'workflow', '店铺商品排行与单品分析入口', ['scope', 'dateRange'], ['itemRanking']],
|
|
36
|
+
['sycm-market', 'market', 'fetch', '类目、市场排行、搜索排行与搜索词取数', ['categoryOrKeyword', 'dateRange'], ['marketData']],
|
|
37
|
+
['sycm-market-competitive-product', 'competitor', 'fetch', '竞品流失、竞争监控与竞店数据取数', ['scope', 'dateRange'], ['competitionData']],
|
|
38
|
+
['weekly-or-monthly-store-performance-reports', 'report', 'report', '店铺经营周报或月报', ['period'], ['markdownReport']]
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export const businessCapabilities = rows.map(([id, domain, kind, summary, inputs, outputs]) => ({
|
|
42
|
+
id, domain, kind, summary, inputs, outputs, sideEffect: 'none'
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
export const mutationCapabilities = [
|
|
46
|
+
{ id: 'qianniu-product-batch-draft', domain: 'product', summary: '批量创建商品草稿', requiredFields: ['products'], entryUrl: 'https://myseller.taobao.com/', steps: ['validate-products', 'open-product-draft-editor', 'create-drafts-only', 'read-back-draft-ids'], verification: 'Every requested product has a returned draft ID; no product is published.' },
|
|
47
|
+
{ id: 'product-migration-guide', domain: 'product', summary: '商品迁移与发布配置', requiredFields: ['sourceProducts', 'targetMode'], entryUrl: 'https://myseller.taobao.com/', steps: ['read-source-products', 'validate-target-category-and-properties', 'create-target-drafts', 'read-back-draft-ids'], verification: 'Target drafts exist and retain the approved category, SKU, media and price configuration.' },
|
|
48
|
+
{ id: 'search-recommend-publish', domain: 'publish', summary: '搜索推荐内容发布', requiredFields: ['content', 'target'], entryUrl: 'https://myseller.taobao.com/', steps: ['validate-content-and-target', 'open-search-recommend-publisher', 'preview', 'publish', 'read-back-publication-status'], verification: 'Publication ID, target and status match the authorized plan.' },
|
|
49
|
+
{ id: 'video-publish-item', domain: 'publish', summary: '商品视频发布', requiredFields: ['itemId', 'video'], entryUrl: 'https://myseller.taobao.com/', steps: ['validate-item-and-video', 'open-item-video-editor', 'upload-or-select-video', 'preview', 'publish', 'read-back-video-status'], verification: 'The bound item shows the expected video identifier and processing or published status.' },
|
|
50
|
+
{ id: 'sucai-apply', domain: 'material', summary: '素材应用到商品或内容', requiredFields: ['materials', 'target'], entryUrl: 'https://myseller.taobao.com/', steps: ['validate-materials-and-target', 'open-material-workspace', 'preview-assignment', 'apply', 'read-back-assignment'], verification: 'The target contains exactly the authorized material identifiers and positions.' },
|
|
51
|
+
{ id: 'shop-index-hosting-manager_2', domain: 'shop', summary: '店铺指标托管配置', requiredFields: ['settings'], entryUrl: 'https://myseller.taobao.com/', steps: ['read-current-settings', 'diff-settings', 'preview', 'apply-settings', 'read-back-settings'], verification: 'Read-back settings equal the authorized settings diff.' },
|
|
52
|
+
{ id: 'mkt_insight-a2ui-price-plan-upload-card', domain: 'promotion', summary: '价格方案上传与应用', requiredFields: ['pricePlan'], entryUrl: 'https://myseller.taobao.com/', steps: ['validate-price-plan', 'open-price-plan-uploader', 'upload-and-parse', 'preview-price-diff', 'apply', 'read-back-price-plan-status'], verification: 'Accepted rows, rejected rows, plan ID and effective status are returned.' }
|
|
53
|
+
].map((entry) => ({ ...entry, kind: 'mutation', sideEffect: 'write', planRequired: true, applyConfirmationRequired: true, excludedActions: ['delete', 'refund', 'cancel', 'account', 'permission', 'payment'] }));
|
|
54
|
+
|
|
55
|
+
export function findBusinessCapability(id) {
|
|
56
|
+
return [...businessCapabilities, ...mutationCapabilities].find((entry) => entry.id === id) || null;
|
|
57
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class CliError extends Error {
|
|
2
|
+
constructor(code, message, { exitCode = 1, details, hint } = {}) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'CliError';
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.exitCode = exitCode;
|
|
7
|
+
this.details = details;
|
|
8
|
+
this.hint = hint;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
toJSON() {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
error: {
|
|
15
|
+
code: this.code,
|
|
16
|
+
message: this.message,
|
|
17
|
+
...(this.details === undefined ? {} : { details: this.details }),
|
|
18
|
+
...(this.hint ? { hint: this.hint } : {})
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function invariant(condition, code, message, options) {
|
|
25
|
+
if (!condition) throw new CliError(code, message, options);
|
|
26
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { CliError, invariant } from './errors.mjs';
|
|
2
|
+
|
|
3
|
+
const operations = [
|
|
4
|
+
['competitor.core-indexes', 'sycm', 'GET', '/mc/mq/rivalItem/analysis/getCoreIndexes.json', ['itemId', 'rivalItemId', 'dateRange']],
|
|
5
|
+
['competitor.core-trend', 'sycm', 'GET', '/mc/mq/rivalItem/analysis/getCoreTrend.json', ['itemId', 'rivalItemId', 'dateRange']],
|
|
6
|
+
['competitor.search', 'sycm', 'GET', '/mc/ci/free/index/rival/item/search.json', ['keyWord']],
|
|
7
|
+
['competitor.recommend', 'sycm', 'GET', '/mc/free/rivalShop/recommend/index/item.json', ['itemId']],
|
|
8
|
+
['competitor.flow-origin', 'sycm', 'GET', '/mc/rivaItem/analysis/getFlowSource/origin/v4/free.json', ['itemId', 'rivalItemId', 'dateRange']],
|
|
9
|
+
['competitor.flow-paid', 'sycm', 'GET', '/mc/rivalItem/getFlowsource/qzt/participate/free.json', ['itemId', 'rivalItemId', 'dateRange']],
|
|
10
|
+
['competitor.flow-source', 'sycm', 'GET', '/mc/mq/rivalItem/analysis/getFlowSource/v4.json', ['itemId', 'rivalItemId', 'dateRange']],
|
|
11
|
+
['competitor.search-words', 'sycm', 'GET', '/mc/mq/rivalItem/analysis/getSearchWords.json', ['itemId', 'rivalItemId', 'dateRange']],
|
|
12
|
+
['alimama.access', 'alimama', 'POST', '/member/checkAccess.json', [], { encoding: 'json', defaultParams: { bizCode: 'universalBP', r: 'mx_13' } }],
|
|
13
|
+
['alimama.campaigns', 'alimama', 'POST', '/campaign/horizontal/findPage.json', [], { encoding: 'json' }],
|
|
14
|
+
['alimama.multi-target-campaigns', 'alimama', 'POST', '/campaign/onebpSite/multiTargCampaignFindPage.json', [], { encoding: 'json' }],
|
|
15
|
+
['alimama.member-config', 'alimama', 'POST', '/member/getMemberConfig.json', [], { encoding: 'json' }],
|
|
16
|
+
['alimama.report-config', 'alimama', 'POST', '/report/getReportConfig.json', [], { encoding: 'json' }],
|
|
17
|
+
['alimama.report', 'alimama', 'POST', '/report/query.json', [], { encoding: 'json' }],
|
|
18
|
+
['alimama.effect-prediction', 'alimama', 'POST', '/algo/listEffectPrediction.json', [], { encoding: 'json' }],
|
|
19
|
+
['alimama.budget-suggestion', 'alimama', 'POST', '/algo/getBatchBudgetSuggestion.json', [], { encoding: 'json' }]
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const alimamaInsightOperations = [
|
|
23
|
+
['alimama.insight.industry-categories', 1638, 'onebpSearch', [], {}],
|
|
24
|
+
['alimama.insight.industry-strategy', 1932, 'universalBP', [], { dateType: '7' }],
|
|
25
|
+
['alimama.insight.industry-profile', 1833, 'universalBP', [], {}],
|
|
26
|
+
['alimama.insight.industry-subcategories', 1640, 'onebpSearch', ['cateLevel1Id'], { cateLevel1Name: '', dateType: '7' }],
|
|
27
|
+
['alimama.insight.industry-traffic-kpi', 1749, 'onebpSearch', ['cateLevel1Id'], {}],
|
|
28
|
+
['alimama.insight.industry-traffic-chart', 1781, 'universalBP', ['cateLevel1Id'], {}],
|
|
29
|
+
['alimama.insight.industry-daily-interpretation', 1748, 'universalBP', ['cateLevel1Id', 'startTime', 'endTime', 'vsStartTime', 'vsEndTime'], {}],
|
|
30
|
+
['alimama.insight.industry-daily-chart', 1782, 'universalBP', ['cateLevel1Id', 'startTime', 'endTime', 'vsStartTime', 'vsEndTime'], {}],
|
|
31
|
+
['alimama.insight.industry-opportunities', 1528, 'universalBP', ['cateLevel1Id', 'cateLevel1Name', 'cateLevel2Id', 'cateLevel2Name'], { dateType: 7 }],
|
|
32
|
+
['alimama.insight.compete-default-category', 1523, 'universalBP', [], {}],
|
|
33
|
+
['alimama.insight.compete-market-penetration', 5589, 'universalBP', ['catelv2Id'], { wordType: 'E', dataType: 1 }],
|
|
34
|
+
['alimama.insight.compete-bubble-chart', 5686, 'universalBP', ['catelv2Id'], { wordType: 'E', dataType: 1 }],
|
|
35
|
+
['alimama.insight.compete-my-items', 5958, 'universalBP', ['catelv2Id'], {}],
|
|
36
|
+
['alimama.insight.compete-excellent-items', 5961, 'universalBP', ['catelv2Id'], { custLevel: '不限', cmpId: '3' }],
|
|
37
|
+
['alimama.insight.traffic-shop-categories', 870, 'universalBP', [], {}],
|
|
38
|
+
['alimama.insight.traffic-rank-list', 1463, 'universalBP', [], { needGray: true, isWordRankShow: 1 }],
|
|
39
|
+
['alimama.insight.traffic-hot-words', 1524, 'universalBP', [], { needGray: true }],
|
|
40
|
+
['alimama.insight.traffic-favorites', 1464, 'universalBP', [], {}],
|
|
41
|
+
['alimama.insight.trend-topic-list', 6687, 'onebpSearch', [], { tab: 'member_item_search' }],
|
|
42
|
+
['alimama.insight.trend-topic-overview', 15884, 'onebpSearch', ['topicId'], { topicName: '', topicCate: '', brandSource: '', brandType: '', tab: 'member_item_search' }],
|
|
43
|
+
['alimama.insight.trend-topic-keywords', 15899, 'onebpSearch', ['topicId'], { topicName: '', topicCate: '', brandSource: '', brandType: '', tab: 'member_item_search' }],
|
|
44
|
+
['alimama.insight.trend-topic-crowd', 4273, 'onebpSearch', ['topicId'], { topicName: '', topicCate: '', brandSource: '', brandType: '', tab: 'member_item_search' }]
|
|
45
|
+
].map(([id, moduleId, bizCode, requiredParams, defaultParams]) => [id, 'alimama', 'POST', '/cube/triggerDynamicModule.json', requiredParams, {
|
|
46
|
+
encoding: 'json', requestShape: 'alimama-dynamic-module', moduleId, bizCode, accessBizCode: 'universalBP', defaultParams
|
|
47
|
+
}]);
|
|
48
|
+
|
|
49
|
+
operations.push(...alimamaInsightOperations);
|
|
50
|
+
|
|
51
|
+
const sycmItemOperations = [
|
|
52
|
+
['item.category-search', '/cc/common/category/getStdCate.json', ['keyWord']],
|
|
53
|
+
['item.diagnose-core', '/cc/diagnose/coreIndex.json', ['itemId', 'dateRange'], { defaultParams: { dateType: 'recent30', crowdType: 'all' } }],
|
|
54
|
+
['item.profile', '/cc/item/archive/profile.json', ['itemId']],
|
|
55
|
+
['item.detail-list', '/cc/item/detail/analysis/list.json', ['itemId', 'dateRange'], { defaultParams: { dateType: 'day', byrType: 'all' } }],
|
|
56
|
+
['item.loss-overview', '/cc/item/detail/analysis/loss/overview.json', ['itemId', 'dateRange']],
|
|
57
|
+
['item.detail-overview', '/cc/item/detail/analysis/overview.json', ['itemId', 'dateRange'], { defaultParams: { dateType: 'day', byrType: 'all' } }],
|
|
58
|
+
['item.main-pic-material', '/cc/item/detail/main/pic/analysis/material.json', ['itemId', 'dateRange'], { defaultParams: { dateType: 'day', byrType: 'all', orderBy: 'itemExposeUv', order: 'desc', page: '1', pageSize: '10', contentType: '', indexCode: 'itemExposeUv,itemInteractUv,itemPayUv,itemLossUv,itemLossRate,itemPayConvertRate' } }],
|
|
59
|
+
['item.rank-live', '/cc/item/live/view/top.json', []],
|
|
60
|
+
['item.price-info', '/cc/item/price/info.json', ['itemId']],
|
|
61
|
+
['item.sale-overview', '/cc/item/sale/overview.json', ['itemId', 'dateRange'], { defaultParams: { dateType: 'day', device: '0' } }],
|
|
62
|
+
['item.title-trend', '/cc/item/title/v2/diagnosis/trend.json', ['itemId', 'dateRange']],
|
|
63
|
+
['item.title-score', '/cc/item/title/v2/titleScore.json', ['itemId']],
|
|
64
|
+
['item.title-word-list', '/cc/item/title/v2/word/list.json', ['itemId', 'dateRange']],
|
|
65
|
+
['item.title-word-recommend', '/cc/item/title/v2/word/recommend.json', ['itemId']],
|
|
66
|
+
['item.title-words', '/cc/item/v2/getTitleWords.json', ['itemId', 'dateRange']],
|
|
67
|
+
['item.dmp-product', '/cc/item/view/dmp/newproduct.json', ['itemId', 'dateRange']],
|
|
68
|
+
['item.dmp-product-top', '/cc/item/view/dmp/newproduct/top.json', ['itemIdList', 'dateRange']],
|
|
69
|
+
['item.rank', '/cc/item/view/top.json', ['dateRange'], { defaultParams: { dateType: 'recent7', page: '1', pageSize: '20' } }],
|
|
70
|
+
['item.sale-live-overview', '/cc/live/item/sale/overview.json', ['itemId']],
|
|
71
|
+
['item.sale-live-attributes', '/cc/live/item/sale/sku/attrDetail.json', ['itemId']],
|
|
72
|
+
['item.sale-live-skus', '/cc/live/v2/item/sale/sku/list.json', ['itemId']],
|
|
73
|
+
['item.rank-long-period', '/cc/long/period/nodistinct/item/view/top.json', ['dateRange']],
|
|
74
|
+
['item.refund-skus', '/cc/refund/item/sku/list.json', ['itemId', 'dateRange']],
|
|
75
|
+
['item.super-sku-overview', '/cc/supersku/overview.json', ['itemId', 'dateRange']],
|
|
76
|
+
['item.refund-close-overview', '/csp/api/refund/item/close/overview.json', ['itemId', 'dateRange']],
|
|
77
|
+
['item.refund-live-overview', '/csp/api/refund/item/live/overview.json', ['itemId']],
|
|
78
|
+
['item.refund-live-trend', '/csp/api/refund/item/live/trend.json', ['itemId']],
|
|
79
|
+
['item.refund-reasons', '/csp/api/refund/item/reason/list/v2.json', ['itemId', 'dateRange']],
|
|
80
|
+
['item.refund-live-warnings', '/csp/api/refund/item/warn/live.json', ['itemId']],
|
|
81
|
+
['item.flow-source-tree', '/flow/item/source/tree/support.json', ['itemId', 'dateRange'], { defaultParams: { dateType: 'day', orderBy: 'uv', order: 'desc', page: '1', pageSize: '10', activateBoost: 'sourceChannel', crowdType: 'all', flowBizType: 'classic', indexCode: 'uv,cltItmCnt,cartByrCnt,payByrCnt' } }],
|
|
82
|
+
['item.flow-source-tree-v3', '/flow/v6/item/crowdtype/source/v3.json', ['itemId', 'dateRange'], { defaultParams: { dateType: 'day', device: '2', belong: 'all', orderBy: 'uv', order: 'desc', page: '1', pageSize: '10', indexCode: 'uv,cartByrCnt,payByrCnt' } }],
|
|
83
|
+
['item.customer-loss-risk', '/mc/item/customers/lossrisk.json', ['itemId', 'dateRange']],
|
|
84
|
+
['item.loss-rank', '/mc/item/detail/analysis/loss/rank.json', ['itemId', 'dateRange']],
|
|
85
|
+
['item.price-band', '/mc/item/price/band/info/v3.json', ['itemId', 'dateRange']],
|
|
86
|
+
['item.aftersales-trend', '/qos/aftersales/item/trend.json', ['itemId', 'dateRange']]
|
|
87
|
+
].map(([id, path, requiredParams, options]) => [id, 'sycm', 'GET', path, requiredParams, options]);
|
|
88
|
+
|
|
89
|
+
const sycmCompetitionOperations = [
|
|
90
|
+
['competition.active-status', '/mc/ci/free/active/status.json', []],
|
|
91
|
+
['competition.valid-group', '/mc/ci/free/config/rival/group/hasValidGroup.json', []],
|
|
92
|
+
['competition.group-items', '/mc/ci/free/config/rival/group/listItemsBySourceType.json', ['sourceType']],
|
|
93
|
+
['competition.monitored-shops-excluding-great', '/mc/ci/free/config/rival/shop/getMonitoredListExcludeGreatShop.json', []],
|
|
94
|
+
['competition.item-monitor-list', '/mc/ci/free/item/monitor/list/page.json', []],
|
|
95
|
+
['competition.item-state', '/mc/ci/free/item/state.json', ['itemId']],
|
|
96
|
+
['competition.shop-monitor-list', '/mc/ci/free/v2/shop/monitor/listShop/page.json', []],
|
|
97
|
+
['competition.search-drain-detail', '/mc/free/ci/item/search/drain/detail.json', ['itemId', 'dateRange']],
|
|
98
|
+
['competition.search-drain-recognition', '/mc/free/ci/item/search/drain/recognition.json', ['dateRange']],
|
|
99
|
+
['competition.view-drain-detail', '/mc/free/ci/item/view/drain/detail.json', ['itemId', 'dateRange']],
|
|
100
|
+
['competition.view-drain-recognition', '/mc/free/ci/item/view/drain/recognition.json', ['dateRange']],
|
|
101
|
+
['competition.item-monitor-list-live', '/mc/free/live/ci/item/monitor/list/page.json', []],
|
|
102
|
+
['competition.shop-monitor-list-live', '/mc/free/live/ci/shop/monitor/listShop/page.json', []],
|
|
103
|
+
['competitor.shop-core-live', '/mc/free/rivalShop/analysis/getCoreIndexesLive.json', ['sellerId']],
|
|
104
|
+
['competitor.shop-trend-live', '/mc/free/rivalShop/analysis/getCoreTrendLive.json', ['sellerId']],
|
|
105
|
+
['competitor.shop-top-items-live', '/mc/free/rivalShop/analysis/getLiveTopItems/page.json', ['sellerId']],
|
|
106
|
+
['competitor.shop-highlight', '/mc/mq/mkt/offline/rival/highlight.json', ['sellerId', 'dateRange']],
|
|
107
|
+
['competitor.shop-operation-summary', '/mc/mq/mkt/offline/rival/operation/summary.json', ['sellerId', 'dateRange']],
|
|
108
|
+
['competitor.shop-strategy', '/mc/mq/mkt/offline/rival/strategy.json', ['sellerId', 'dateRange']],
|
|
109
|
+
['competitor.group-core-indexes', '/mc/mq/rivalItem/analysis/group/getCoreIndexes.json', ['groupId', 'dateRange']],
|
|
110
|
+
['competitor.shop-core', '/mc/mq/rivalShop/analysis/getCoreIndexes.json', ['sellerId', 'dateRange']],
|
|
111
|
+
['competitor.shop-trend', '/mc/mq/rivalShop/analysis/getCoreTrend.json', ['sellerId', 'dateRange']],
|
|
112
|
+
['competitor.shop-flow-source', '/mc/mq/rivalShop/analysis/getFlowSource/v4.json', ['sellerId', 'dateRange']],
|
|
113
|
+
['competitor.shop-search-words', '/mc/mq/rivalShop/analysis/getSearchWords.json', ['sellerId', 'dateRange']],
|
|
114
|
+
['competitor.shop-crowd', '/mc/mq/rivalShop/analysis/getShopCrowd.json', ['sellerId', 'dateRange']],
|
|
115
|
+
['competitor.shop-top-items', '/mc/mq/rivalShop/analysis/getTopItems/page.json', ['sellerId', 'dateRange']],
|
|
116
|
+
['competitor.shop-profile-check', '/mc/mq/rivalShop/checkProfile.json', ['sellerId']],
|
|
117
|
+
['competitor.shop-profile', '/mc/mq/rivalShop/profile/detail.json', ['sellerId']],
|
|
118
|
+
['competitor.shop-flow-origin', '/mc/rivalShop/analysis/getFlowSource/origin/v4/free.json', ['sellerId', 'dateRange']],
|
|
119
|
+
['competitor.shop-flow-overview', '/mc/rivalShop/analysis/getFlowSource/overview/v4.json', ['sellerId', 'dateRange']],
|
|
120
|
+
['competitor.shop-flow-paid', '/mc/rivalShop/getFlowsource/qzt/participate/free.json', ['sellerId', 'dateRange']]
|
|
121
|
+
].map(([id, path, requiredParams]) => [id, 'sycm', 'GET', path, requiredParams]);
|
|
122
|
+
|
|
123
|
+
operations.push(...sycmItemOperations, ...sycmCompetitionOperations);
|
|
124
|
+
|
|
125
|
+
operations.push(
|
|
126
|
+
['shop.trade-summary', 'sycm-trade', 'GET', '/bda/tradinganaly/overview/get_summary.json', ['dateRange'], { defaultParams: { dateType: 'recent7', device: '0' } }],
|
|
127
|
+
['shop.trade-categories', 'sycm-trade', 'GET', '/bda/tradinganaly/overview/getCateTop.json', ['dateRange'], { defaultParams: { dateType: 'recent7' } }],
|
|
128
|
+
['shop.trade-trend', 'sycm-trade', 'GET', '/bda/tradinganaly/overview/getTendency.json', ['categoryId', 'dateRange', 'indexs'], { defaultParams: { dateType: 'recent7', device: '0', diff: '0' } }]
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
operations.push(
|
|
132
|
+
['dmp.shop-standard-categories', 'dmp', 'GET', '/api/dmp/shop/std/cate', [], { defaultParams: { cateModel: '2', filterModel: '0' } }],
|
|
133
|
+
['dmp.shop-categories', 'dmp', 'GET', '/api/goods/cate/list', ['endDate'], { defaultParams: { period: '30', page: '1', pageSize: '10' } }],
|
|
134
|
+
['dmp.shop-date-range', 'dmp', 'GET', '/api/goods/shop/dateRange', []],
|
|
135
|
+
['dmp.shop-item-indicators', 'dmp', 'GET', '/api/goods/shop/item/indicators', [], { defaultParams: { type: '1' } }],
|
|
136
|
+
['dmp.shop-items', 'dmp', 'GET', '/api/goods/shop/item/list', ['endDate'], { defaultParams: { period: '30', page: '1', pageSize: '20' } }],
|
|
137
|
+
['dmp.shop-labels', 'dmp', 'GET', '/api/goods/shop/label/list', []],
|
|
138
|
+
['dmp.shop-overview', 'dmp', 'GET', '/api/goods/shop/v3/overview', ['endDate'], { defaultParams: { period: '30', benchmark: '0' } }]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
operations.push(
|
|
142
|
+
['coupon.page-detail', 'coupon', 'GET', '/consumercoupon/renderCouponPageDetailView.do', []],
|
|
143
|
+
['coupon.fee-init', 'coupon', 'GET', '/consumercoupon/getFeeInitInfo.do', []],
|
|
144
|
+
['coupon.fee-groups', 'coupon', 'GET', '/consumercoupon/getFeeGroupList.do', ['startTime', 'endTime']],
|
|
145
|
+
['coupon.fee-rule', 'coupon', 'GET', '/consumercoupon/getFeeRuleInfo.do', ['date']],
|
|
146
|
+
['coupon.fee-rules', 'coupon', 'GET', '/consumercoupon/getFeeRuleInfoList.do', ['startTime', 'endTime']],
|
|
147
|
+
['coupon.day-fee-summary', 'coupon', 'GET', '/consumercoupon/getDayFeeInfo.do', ['time']],
|
|
148
|
+
['coupon.day-fee-detail', 'coupon', 'GET', '/consumercoupon/getDayFeeInfoDetail.do', ['time']],
|
|
149
|
+
['coupon.day-fee-list', 'coupon', 'GET', '/consumercoupon/getDayFeeInfoListV2.do', ['startTime', 'endTime']],
|
|
150
|
+
['coupon.detail-day-list', 'coupon', 'GET', '/consumercoupon/getDayVOList.do', ['startTime', 'endTime']],
|
|
151
|
+
['coupon.download-tasks', 'coupon', 'GET', '/list/asyncTaskList/queryListData.do', ['processorKey', 'domainId']]
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const services = {
|
|
155
|
+
sycm: { origin: 'https://sycm.taobao.com', entryUrl: 'https://sycm.taobao.com/mc/mq/market_monitor.htm', hosts: ['sycm.taobao.com'], tokenStrategy: 'sycm-meta' },
|
|
156
|
+
'sycm-trade': { origin: 'https://sycm.taobao.com', entryUrl: 'https://sycm.taobao.com/fa/trade/detail?activeKey=overview', hosts: ['sycm.taobao.com'], tokenStrategy: 'sycm-meta' },
|
|
157
|
+
alimama: { origin: 'https://one.alimama.com', entryUrl: 'https://one.alimama.com/', hosts: ['one.alimama.com'], tokenStrategy: 'alimama-access' },
|
|
158
|
+
dmp: { origin: 'https://dmp.advgateway.taobao.com', entryUrl: 'https://dmp.taobao.com/index_new.html#!/items/shop-insight', hosts: ['dmp.taobao.com'], tokenStrategy: 'dmp-magix' },
|
|
159
|
+
coupon: { origin: 'https://sale.taobao.com', entryUrl: 'https://myseller.taobao.com/home.htm/coupon_marketing/home?tab=fee', hosts: ['myseller.taobao.com', 'sale.taobao.com'], matchPath: '/home.htm/coupon_marketing/', tokenStrategy: 'taobao-cookie' }
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const operationRegistry = Object.freeze(Object.fromEntries(operations.map(([id, service, method, path, requiredParams, options = {}]) => [id, {
|
|
163
|
+
id, service, method, path, requiredParams, sideEffect: 'none', ...options
|
|
164
|
+
}])));
|
|
165
|
+
|
|
166
|
+
export function getService(id) {
|
|
167
|
+
const service = services[id];
|
|
168
|
+
if (!service) throw new CliError('UNKNOWN_SERVICE', `Unknown browser service: ${id}`, { exitCode: 2 });
|
|
169
|
+
return service;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function getOperation(id) {
|
|
173
|
+
const operation = operationRegistry[id];
|
|
174
|
+
if (!operation) throw new CliError('UNKNOWN_OPERATION', `Unknown registered operation: ${id}`, { exitCode: 2, hint: 'Run sycmcli data operations.' });
|
|
175
|
+
return operation;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function validateOperationParams(operation, params) {
|
|
179
|
+
invariant(params && typeof params === 'object' && !Array.isArray(params), 'INVALID_ARGUMENT', 'Operation params must be a JSON object.', { exitCode: 2 });
|
|
180
|
+
const missing = operation.requiredParams.filter((key) => params[key] === undefined || params[key] === null || params[key] === '');
|
|
181
|
+
invariant(missing.length === 0, 'MISSING_ARGUMENT', `Missing operation parameters: ${missing.join(', ')}.`, { exitCode: 2 });
|
|
182
|
+
if (operation.id.startsWith('alimama.insight.industry-')) {
|
|
183
|
+
if (params.dateType !== undefined) invariant([7, 15, 30, '7', '15', '30'].includes(params.dateType), 'INVALID_ARGUMENT', 'Industry dateType must be 7, 15, or 30.', { exitCode: 2 });
|
|
184
|
+
for (const key of ['startTime', 'endTime', 'vsStartTime', 'vsEndTime']) {
|
|
185
|
+
const value = params[key];
|
|
186
|
+
if (value !== undefined && value !== null) invariant(typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value, 'INVALID_ARGUMENT', `${key} must be a valid YYYY-MM-DD date.`, { exitCode: 2 });
|
|
187
|
+
}
|
|
188
|
+
if (params.startTime && params.endTime) invariant(params.startTime <= params.endTime, 'INVALID_ARGUMENT', 'startTime must not follow endTime.', { exitCode: 2 });
|
|
189
|
+
if (params.vsStartTime && params.vsEndTime) invariant(params.vsStartTime <= params.vsEndTime, 'INVALID_ARGUMENT', 'vsStartTime must not follow vsEndTime.', { exitCode: 2 });
|
|
190
|
+
}
|
|
191
|
+
return params;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function listOperations() {
|
|
195
|
+
return Object.values(operationRegistry).map((operation) => ({ ...operation, ...getService(operation.service), origin: getService(operation.service).origin }));
|
|
196
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"package": "@petercjl/sycmcli",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": "https://github.com/petercjl/sycmcli",
|
|
6
|
+
"files": {
|
|
7
|
+
"operation-registry.mjs": "bd41f205b22e1f6c02661defa61b39523f4714d839510ba61991575d2a34ff00",
|
|
8
|
+
"business-capabilities.mjs": "e3b1fec7152f5cc8f1f1aef7c36ebce42e5a0cb620e217c074b32921b0a15d2e",
|
|
9
|
+
"errors.mjs": "098faaf49430f93acdfc6601920a7e3a6e6baa6f978dbcc1b81b7d87ee1d6974",
|
|
10
|
+
"LICENSE": "cb0b6cc0ee0a14ca9f59f841b3743b9974968009ae536a7d27d6457f54e9ff39"
|
|
11
|
+
}
|
|
12
|
+
}
|
package/lib/sycm.mjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import {fail,exists,privatePath,writeNew} from './common.mjs';
|
|
6
|
+
import {discover,requestInConnection,registeredReadInConnection,openBusinessPageInConnection,callMailbox,resolveContainer} from './browser.mjs';
|
|
7
|
+
import {listOperations,operationRegistry,getService,validateOperationParams} from './sycm-vendor/operation-registry.mjs';
|
|
8
|
+
import {businessCapabilities,mutationCapabilities} from './sycm-vendor/business-capabilities.mjs';
|
|
9
|
+
|
|
10
|
+
const ORIGIN='https://sycm.taobao.com';
|
|
11
|
+
// Additional first-class reads implemented outside the source registry in src/sycm.mjs.
|
|
12
|
+
const legacyReads=[
|
|
13
|
+
['category.tree','/mc/common/free/getCateInfo.json',[],{marketVersion:'free'}],
|
|
14
|
+
['category.main','/portal/shop/getMainCateInfo.json',[],{}],
|
|
15
|
+
['market.item-rank','/mc/mq/mkt/item/offline/rank.json',['cateId','dateRange'],{rankType:'gmv',indexCode:'payByrCnt,uv',page:1,pageSize:10,marketVersion:'free',sellerType:'-1',cateFlag:'2'}],
|
|
16
|
+
['market.item-flow-rank','/mc/mq/mkt/item/offline/rank/search.json',['cateId','dateRange'],{rankType:'flow',indexCode:'uv,searchUv',page:1,pageSize:10,marketVersion:'free',sellerType:'-1',cateFlag:'2'}],
|
|
17
|
+
['market.item-add-rank','/mc/mq/mkt/item/offline/rank/purpose.json',['cateId','dateRange'],{rankType:'add',indexCode:'cartByrCnt,cltByrCnt,uv',page:1,pageSize:10,marketVersion:'free',sellerType:'-1',cateFlag:'2'}],
|
|
18
|
+
['market.price-segments','/mc/mq/mkt/priceSeg/list.json',['cateId','dateRange'],{sellerType:'0',marketVersion:'free'}],
|
|
19
|
+
['market.keyword-rank','/mc/mq/mkt/keyword/rank/pro.json',['cateId','dateRange'],{kwType:'search',order:'desc',orderBy:'seIpvUvHits',rankType:'hot',page:1,pageSize:10,marketVersion:'free',device:'0'}],
|
|
20
|
+
['word.overview','/mc/free/searchword/overview.json',['keyword','dateRange'],{device:'0',marketVersion:'free'}],
|
|
21
|
+
['word.trend','/mc/free/searchword/propertyTrend.json',['keyword','dateRange'],{device:'0',marketVersion:'free'}],
|
|
22
|
+
['word.related','/mc/mq/mkt/keyword/relate/analysis.json',['keyWord','dateRange'],{cycleFlag:'cycle',order:'desc',orderBy:'seIpvUvHits',rankType:'related',page:1,pageSize:10,device:'0',marketVersion:'free'}],
|
|
23
|
+
['word.category','/mc/mq/mkt/keyword/cate/analysis.json',['keyWord','dateRange'],{order:'desc',orderBy:'clickHitsRatio',page:1,pageSize:10,device:'0',marketVersion:'free'}]
|
|
24
|
+
].map(([id,path,requiredParams,defaultParams])=>({id,service:'sycm',method:'GET',path,requiredParams,defaultParams,sideEffect:'none',source:'src/sycm.mjs'}));
|
|
25
|
+
const allOperations=()=>[...listOperations(),...legacyReads];
|
|
26
|
+
const resolveOperation=id=>{
|
|
27
|
+
const found=legacyReads.find(x=>x.id===id)??(Object.hasOwn(operationRegistry,id)?operationRegistry[id]:null);
|
|
28
|
+
if(!found)throw fail('UNKNOWN_OPERATION','Choose a registered operation from sycm operations');
|
|
29
|
+
return found;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const sensitive=/token|cookie|csrf|password|passwd|authorization|secret|sessionid|credential|api[-_]?key|^__d?tk$|^session$|^loginpointid$/i;
|
|
33
|
+
export function assertPublicParams(value) {
|
|
34
|
+
if(Array.isArray(value)){for(const child of value)assertPublicParams(child);return;}
|
|
35
|
+
if(value&&typeof value==='object')for(const [key,child]of Object.entries(value)) {
|
|
36
|
+
if(sensitive.test(key)||['__proto__','constructor','prototype'].includes(key))throw fail('CREDENTIAL_INPUT_REJECTED','Use browser-managed authentication; credential or prototype fields are forbidden');
|
|
37
|
+
assertPublicParams(child);
|
|
38
|
+
}
|
|
39
|
+
if(typeof value==='string'&&/(?:token|cookie|csrf(?:id)?|password|authorization|__tk|__dtk|loginpointid)=/i.test(value))throw fail('CREDENTIAL_INPUT_REJECTED','Credential-bearing strings are forbidden');
|
|
40
|
+
}
|
|
41
|
+
export function paramsObject(value) {
|
|
42
|
+
if(value===undefined)return {};
|
|
43
|
+
let text=value;
|
|
44
|
+
if(!text.trimStart().startsWith('{')) {
|
|
45
|
+
const info=fs.lstatSync(text);
|
|
46
|
+
if(!info.isFile()||info.size>262144)throw fail('INVALID_ARGUMENT','Parameter input must be a JSON object or a file at most 256 KiB');
|
|
47
|
+
text=fs.readFileSync(text,'utf8');
|
|
48
|
+
}
|
|
49
|
+
if(Buffer.byteLength(text)>262144)throw fail('INVALID_ARGUMENT','Parameter input is too large');
|
|
50
|
+
let result;try{result=JSON.parse(text);}catch{throw fail('INVALID_ARGUMENT','Invalid parameter JSON');}
|
|
51
|
+
if(!result||typeof result!=='object'||Array.isArray(result))throw fail('INVALID_ARGUMENT','Parameters must be a JSON object');
|
|
52
|
+
assertPublicParams(result);return result;
|
|
53
|
+
}
|
|
54
|
+
const transportFor=op=>op.method==='GET'&&getService(op.service).origin===ORIGIN?'native-session-get':'adapter-required';
|
|
55
|
+
export function operationSchema(id) {
|
|
56
|
+
const op=resolveOperation(id), service=getService(op.service);
|
|
57
|
+
return {...op,origin:service.origin,entryUrl:service.entryUrl,tokenStrategy:service.tokenStrategy,transport:'native-business-read',legacyTransport:transportFor(op),runtimeVerified:false,
|
|
58
|
+
minimumBrowserCapability:'businessReadProtocol=1 with matching registry hash',limits:'Credentials remain native. Per-endpoint permissions, CSRF, date semantics and service login require real validation.'};
|
|
59
|
+
}
|
|
60
|
+
export function nativeRegistry() {
|
|
61
|
+
return Object.fromEntries(inventory().operations.map(op=>[op.id,{
|
|
62
|
+
id:op.id,service:op.service,method:op.method,path:op.path,origin:op.origin,
|
|
63
|
+
entryUrl:op.entryUrl,tokenStrategy:op.tokenStrategy,requiredParams:op.requiredParams,
|
|
64
|
+
defaultParams:op.defaultParams??{},...(op.encoding?{encoding:op.encoding}:{}),
|
|
65
|
+
...(op.requestShape?{requestShape:op.requestShape,moduleId:op.moduleId,bizCode:op.bizCode,accessBizCode:op.accessBizCode}:{})
|
|
66
|
+
}]));
|
|
67
|
+
}
|
|
68
|
+
export const nativeRegistryHash=()=>crypto.createHash('sha256').update(JSON.stringify(nativeRegistry())).digest('hex');
|
|
69
|
+
export function inventory() {
|
|
70
|
+
return {ok:true,schemaVersion:1,source:{package:'@petercjl/sycmcli',version:'0.4.0',license:'MIT'},
|
|
71
|
+
operations:allOperations().map(op=>operationSchema(op.id)),businessCapabilities,
|
|
72
|
+
mutations:mutationCapabilities.map(x=>({...x,execution:'plan-and-authorization-only',automaticSubmission:false}))};
|
|
73
|
+
}
|
|
74
|
+
export function buildRead(id,params) {
|
|
75
|
+
const op=resolveOperation(id);assertPublicParams(params);validateOperationParams(op,params);
|
|
76
|
+
if((id.startsWith('market.')||['word.related','word.category'].includes(id))&¶ms.pageSize!==undefined&&(!Number.isInteger(Number(params.pageSize))||Number(params.pageSize)<1||Number(params.pageSize)>20))throw fail('INVALID_ARGUMENT','Market pageSize must be an integer from 1 through 20');
|
|
77
|
+
if(transportFor(op)!=='native-session-get')throw fail('SERVICE_ADAPTER_REQUIRED','This registered operation requires a native '+op.service+' adapter; no alternate browser will be opened');
|
|
78
|
+
const url=new URL(op.path,ORIGIN);
|
|
79
|
+
for(const [key,value]of Object.entries({...op.defaultParams,...params})) {
|
|
80
|
+
if(value===undefined||value===null)continue;
|
|
81
|
+
if(!['string','number','boolean'].includes(typeof value))throw fail('INVALID_ARGUMENT','Native GET parameters must be scalar values');
|
|
82
|
+
url.searchParams.set(key,String(value));
|
|
83
|
+
}
|
|
84
|
+
return {operation:id,service:op.service,url:url.href};
|
|
85
|
+
}
|
|
86
|
+
function sanitized(value) {
|
|
87
|
+
if(Array.isArray(value))return value.map(sanitized);
|
|
88
|
+
if(value&&typeof value==='object')return Object.fromEntries(Object.entries(value).map(([key,child])=>[key,sensitive.test(key)?'[REDACTED]':sanitized(child)]));
|
|
89
|
+
if(typeof value==='string'&&/(?:token|cookie|csrf(?:id)?|password|authorization|__tk|__dtk|loginpointid)=/i.test(value))return '[REDACTED]';
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
export function businessResponse(value) {
|
|
93
|
+
if(!value||typeof value!=='object'||Array.isArray(value))throw fail('BUSINESS_RESPONSE_INVALID','Missing business envelope',{executed:true});
|
|
94
|
+
for(const envelope of [value,value.content].filter(x=>x&&typeof x==='object')) {
|
|
95
|
+
const failed=(envelope.code!==undefined&&envelope.code!==null&&![0,200,'0','200','SUCCESS'].includes(envelope.code))||envelope.success===false||envelope.info?.ok===false||envelope.hasError===true;
|
|
96
|
+
if(failed) {
|
|
97
|
+
const text=String(envelope.message??envelope.msg??envelope.info?.message??'');
|
|
98
|
+
const code=/login|登录|会话|session/i.test(text)?'AUTH_REQUIRED':/验证|风控|captcha|risk/i.test(text)?'RISK_VERIFICATION_REQUIRED':/权限|授权|permission|forbidden/i.test(text)?'PERMISSION_REQUIRED':'BUSINESS_REQUEST_FAILED';
|
|
99
|
+
throw fail(code,'Platform rejected the registered read; resolve login, verification or permissions in the selected container',{executed:true});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if(!Object.hasOwn(value,'data')&&!Object.hasOwn(value,'result')&&!Object.hasOwn(value.content??{},'data'))throw fail('BUSINESS_RESPONSE_INVALID','Unrecognized business data envelope',{executed:true});
|
|
103
|
+
return sanitized(value);
|
|
104
|
+
}
|
|
105
|
+
const template=value=>typeof value==='string'&&/^##[^#]+##$/.test(value);
|
|
106
|
+
const hasTemplate=value=>template(value)||Boolean(value&&typeof value==='object'&&Object.values(value).some(hasTemplate));
|
|
107
|
+
// Template semantics follow the pinned sycmcli MIT Alimama insight reader.
|
|
108
|
+
export function resolveModuleResponse(response,flows) {
|
|
109
|
+
const results=flows.flatMap(flow=>businessResponse(flow.response)?.data?.resultList??[]);
|
|
110
|
+
const resolve=value=>{
|
|
111
|
+
if(template(value)) {
|
|
112
|
+
const ref=value.slice(2,-2),index=ref.match(/^result\[(\d+)\]$/);
|
|
113
|
+
if(index) {
|
|
114
|
+
for(const candidate of results)if(Array.isArray(candidate?.result)&&candidate.result[Number(index[1])]!==undefined)return candidate.result[Number(index[1])];
|
|
115
|
+
return results[Number(index[1])]??value;
|
|
116
|
+
}
|
|
117
|
+
for(const candidate of results) {
|
|
118
|
+
let current=candidate;
|
|
119
|
+
for(const key of ref.replace(/\[(\d+)\]/g,'.$1').split('.'))current=current&&typeof current==='object'&&Object.hasOwn(current,key)?current[key]:undefined;
|
|
120
|
+
if(current!==undefined)return current;
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
if(Array.isArray(value))return value.map(resolve);
|
|
125
|
+
if(value&&typeof value==='object')return Object.fromEntries(Object.entries(value).map(([key,child])=>[key,resolve(child)]));
|
|
126
|
+
return value;
|
|
127
|
+
};
|
|
128
|
+
const resolved=resolve(response);
|
|
129
|
+
if(hasTemplate(resolved)&&!flows.length)throw fail('DATA_FLOW_EXPANSION_REQUIRED','Registered module needs native data-flow expansion; retain the operation ID and inspect browser support',{executed:true});
|
|
130
|
+
return sanitized(resolved);
|
|
131
|
+
}
|
|
132
|
+
export async function runRead(options,deps={}) {
|
|
133
|
+
const params=paramsObject(options['params-json']),op=resolveOperation(options.operation);
|
|
134
|
+
assertPublicParams(params);validateOperationParams(op,params);
|
|
135
|
+
if(Buffer.byteLength(JSON.stringify(params))>24576)throw fail('INVALID_ARGUMENT','Read parameters exceed the native 24 KiB limit');
|
|
136
|
+
const connection=(deps.discover??discover)(options);
|
|
137
|
+
let result;
|
|
138
|
+
if(connection.endpoint.businessReadProtocol===1)result=await (deps.registeredRequest??registeredReadInConnection)(connection,options,op.id,params,nativeRegistryHash());
|
|
139
|
+
else {
|
|
140
|
+
if(transportFor(op)!=='native-session-get')throw fail('BROWSER_UPGRADE_REQUIRED','Upgrade Ecom to native business read protocol 1 for this registered service; keep the existing container login');
|
|
141
|
+
const built=buildRead(options.operation,params);
|
|
142
|
+
result=await (deps.request??requestInConnection)(connection,{...options,url:built.url,method:'GET'});
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const data=businessResponse(result.data),flows=result.dataFlows??[];
|
|
146
|
+
const expanded=op.requestShape==='alimama-dynamic-module'&&hasTemplate(data)?resolveModuleResponse(data,flows):undefined;
|
|
147
|
+
const partial=Boolean(expanded&&hasTemplate(expanded));
|
|
148
|
+
return {...result,...(flows.length?{dataFlows:sanitized(flows)}:{}),...(expanded?{resolvedResponse:expanded,completeness:{complete:!partial,scope:'registered-module-response',...(partial?{reason:'Platform data flows returned successfully but some template references have no returned value; inspect resolvedResponse and do not infer missing values.'}:{})}}:{}),operation:op.id,service:op.service,data,businessStatus:partial?'partial':'success',fetchedAt:new Date().toISOString(),dataDateVerification:'caller-must-check-requested-period-and-platform-update-time'};
|
|
149
|
+
}catch(error){error.operationId=result.operationId;throw error;}
|
|
150
|
+
}
|
|
151
|
+
export async function openOperation(options,deps={}) {
|
|
152
|
+
const op=operationSchema(options.operation),connection=(deps.discover??discover)(options);
|
|
153
|
+
return (deps.open??openBusinessPageInConnection)(connection,options,op.id,op.entryUrl,nativeRegistryHash());
|
|
154
|
+
}
|
|
155
|
+
async function account(connection,selector) {
|
|
156
|
+
const list=await callMailbox(connection,{method:'shops.list'});
|
|
157
|
+
if(!Array.isArray(list.shops))throw fail('PROTOCOL_MISMATCH','Missing account metadata');
|
|
158
|
+
const row=resolveContainer(list.shops,selector);
|
|
159
|
+
await callMailbox(connection,{method:'shops.verify',containerId:row.id},60000);
|
|
160
|
+
const refreshed=await callMailbox(connection,{method:'shops.list'});
|
|
161
|
+
const current=resolveContainer(refreshed.shops,row.id);
|
|
162
|
+
if(current.state!=='ready'||!current.identity?.shopId||!current.identity?.accountId)throw fail('AUTH_REQUIRED','A verified bound platform identity is required');
|
|
163
|
+
return {id:current.id,name:current.name,identity:{shopId:String(current.identity.shopId),accountId:String(current.identity.accountId)}};
|
|
164
|
+
}
|
|
165
|
+
function planDirectory(create=false) {
|
|
166
|
+
const state=process.env.ECOM_CLI_STATE_DIR??path.join(os.homedir(),'.ecom-cli');
|
|
167
|
+
if(!path.isAbsolute(state))throw fail('INVALID_ARGUMENT','State directory must be absolute');
|
|
168
|
+
const dir=path.join(state,'sycm-plans');
|
|
169
|
+
for(let p=dir;p!==path.dirname(p);p=path.dirname(p))if(exists(p)&&fs.lstatSync(p).isSymbolicLink()&&!(process.platform==='darwin'&&['/var','/tmp'].includes(p)&&fs.lstatSync(p).uid===0))throw fail('UNSAFE_PATH','Plan state must use physical directories');
|
|
170
|
+
if(create)fs.mkdirSync(dir,{recursive:true,mode:0o700});privatePath(dir,true);return dir;
|
|
171
|
+
}
|
|
172
|
+
function planFile(id) {
|
|
173
|
+
if(!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(id??''))throw fail('INVALID_ARGUMENT','Invalid plan ID');
|
|
174
|
+
return path.join(planDirectory(),id+'.json');
|
|
175
|
+
}
|
|
176
|
+
const digest=value=>crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
177
|
+
function mutation(id) {
|
|
178
|
+
const item=mutationCapabilities.find(x=>x.id===id);
|
|
179
|
+
if(!item)throw fail('UNKNOWN_MUTATION','Choose a registered mutation from sycm capabilities');
|
|
180
|
+
return item;
|
|
181
|
+
}
|
|
182
|
+
export async function planMutation(options,deps={}) {
|
|
183
|
+
const cap=mutation(options.operation),payload=paramsObject(options['params-json']);
|
|
184
|
+
for(const key of cap.requiredFields)if(payload[key]===undefined||payload[key]===null)throw fail('INVALID_ARGUMENT','Missing mutation field: '+key);
|
|
185
|
+
const connection=(deps.discover??discover)(options),target=await (deps.account??account)(connection,options.container);
|
|
186
|
+
const planId=crypto.randomUUID(),confirmationCode=crypto.randomBytes(4).toString('hex').toUpperCase(),now=Date.now();
|
|
187
|
+
const plan={schemaVersion:1,planId,capabilityId:cap.id,idempotencyKey:crypto.randomUUID(),container:target,instanceId:connection.endpoint.instanceId,payload,payloadDigest:digest(payload),confirmationDigest:digest({planId,confirmationCode}),createdAt:new Date(now).toISOString(),expiresAt:new Date(now+900000).toISOString()};
|
|
188
|
+
planDirectory(true);
|
|
189
|
+
writeNew(planFile(planId),plan);
|
|
190
|
+
return {ok:true,plan:{...publicPlan(plan),confirmationCode,payload:sanitized(payload)},executed:false,automaticSubmission:false};
|
|
191
|
+
}
|
|
192
|
+
function loadPlan(id) {const file=planFile(id);privatePath(file);return JSON.parse(fs.readFileSync(file,'utf8'));}
|
|
193
|
+
function publicPlan(plan) {
|
|
194
|
+
const {payload,confirmationDigest,...result}=plan;
|
|
195
|
+
return {...result,payloadFields:Object.keys(payload).sort(),status:exists(planFile(plan.planId)+'.authorized')?'authorized':'planned'};
|
|
196
|
+
}
|
|
197
|
+
export function showPlan(id) {return {ok:true,plan:publicPlan(loadPlan(id)),executed:false,automaticSubmission:false};}
|
|
198
|
+
export async function authorizePlan(options,deps={}) {
|
|
199
|
+
const plan=loadPlan(options.plan),cap=mutation(plan.capabilityId),file=planFile(plan.planId)+'.authorized';
|
|
200
|
+
if(exists(file))throw fail('PLAN_ALREADY_USED','Plan already authorized; inspect it before further action');
|
|
201
|
+
if(Date.parse(plan.expiresAt)<=Date.now())throw fail('PLAN_EXPIRED','Create and review a new plan');
|
|
202
|
+
if(plan.payloadDigest!==digest(plan.payload))throw fail('PLAN_TAMPERED','Payload digest mismatch');
|
|
203
|
+
if(plan.confirmationDigest!==digest({planId:plan.planId,confirmationCode:(options.confirm??'').toUpperCase()}))throw fail('CONFIRMATION_FAILED','Wrong confirmation code');
|
|
204
|
+
const connection=(deps.discover??discover)(options),target=await (deps.account??account)(connection,options.container);
|
|
205
|
+
if(connection.endpoint.instanceId!==plan.instanceId||target.id!==plan.container.id||JSON.stringify(target.identity)!==JSON.stringify(plan.container.identity))throw fail('STORE_IDENTITY_MISMATCH','Instance, container or platform identity differs from the reviewed plan');
|
|
206
|
+
try{writeNew(file,{planId:plan.planId,authorizedAt:new Date().toISOString(),payloadDigest:plan.payloadDigest});}catch(e){if(e.code==='EEXIST')throw fail('PLAN_ALREADY_USED','Plan already authorized');throw e;}
|
|
207
|
+
return {ok:true,planId:plan.planId,status:'authorized',executed:false,automaticSubmission:false,container:target,payload:sanitized(plan.payload),workflow:{...cap,readbackRequired:true},nextAction:'Execute only the reviewed workflow using an explicitly supported browser capability; return real readback evidence. This command performs no platform mutation.'};
|
|
208
|
+
}
|
package/package.json
CHANGED
package/scripts/validate.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import {inventory,nativeRegistryHash} from '../lib/sycm.mjs';
|
|
4
6
|
import {root,pkg,tree} from '../lib/common.mjs';
|
|
5
7
|
const caps=JSON.parse(fs.readFileSync(path.join(root,'capabilities.json'),'utf8'));
|
|
6
8
|
const skill=JSON.parse(fs.readFileSync(path.join(root,'skills/ecom-cli/package.json'),'utf8'));
|
|
@@ -8,3 +10,13 @@ assert.equal(caps.version,pkg.version);assert.equal(skill.version,pkg.version);
|
|
|
8
10
|
assert.equal(skill.name,pkg.name);assert.ok(fs.statSync(path.join(root,'skills/ecom-cli/SKILL.md')).size>100);
|
|
9
11
|
tree(path.join(root,'skills')); // Reject shared links before packing.
|
|
10
12
|
console.error('Version, Skill identity and physical resource checks passed: '+pkg.version);
|
|
13
|
+
|
|
14
|
+
const vendor=path.join(root,'lib/sycm-vendor');
|
|
15
|
+
const provenance=JSON.parse(fs.readFileSync(path.join(vendor,'provenance.json'),'utf8'));
|
|
16
|
+
for(const [name,hash]of Object.entries(provenance.files)) {
|
|
17
|
+
assert.equal(crypto.createHash('sha256').update(fs.readFileSync(path.join(vendor,name))).digest('hex'),hash,'Pinned source changed without updating provenance');
|
|
18
|
+
}
|
|
19
|
+
assert.equal(inventory().operations.length,caps.businessIntegration.registeredReads);
|
|
20
|
+
assert.equal(nativeRegistryHash(),caps.businessIntegration.nativeRegistryHash);
|
|
21
|
+
assert.ok(inventory().operations.every(x=>x.sideEffect==='none'&&['GET','POST'].includes(x.method)));
|
|
22
|
+
console.error('Pinned business catalog and native identity checks passed: '+nativeRegistryHash());
|
package/skills/ecom-cli/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ecom-cli
|
|
3
|
-
description:
|
|
3
|
+
description: Use Ecom account containers for registered Shengyicanmou data reads, local mutation planning and confirmation, account inspection, native-session requests, or CLI and companion Skill maintenance. Business analysis and operating decisions belong to the caller.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Ecom CLI
|
|
@@ -18,8 +18,22 @@ Input is an Ecom container listing/detail request, an authorized read-only endpo
|
|
|
18
18
|
5. For a session request, require an explicit authorized read-only endpoint from the user or a trusted business capability’s registered operation. Business selection, endpoint semantics, date/metrics and interpretation belong to that caller. Run `session request --container VALUE --url HTTPS_URL --method GET --json`. GET alone does not prove an endpoint has no side effects; resolve unclear endpoint intent before submission. The CLI pins one instance and container across discovery and execution. Credentials remain native; header/body/credential inputs, redirects and HTML are unsupported. The enabled origin is controlled by Ecom, not expanded by the CLI.
|
|
19
19
|
6. Require `ok=true`, protocol version 1, operation ID and matching instance/container IDs. For requests, require the JSON response contract and distinguish HTTP success from business success: `businessStatus=not-evaluated` requires the caller’s platform-specific check. Preserve structured errors and executed status. These operations do not navigate or activate tabs.
|
|
20
20
|
|
|
21
|
+
## Registered business operations
|
|
22
|
+
|
|
23
|
+
For business data or mutation-planning requests, read [the business contract](references/sycm.md), then return to main-line target selection. The business registry is bundled with this runtime; no external sycmcli installation, Skill or browser is required. `sycm capabilities --json` exposes the complete imported catalog and native/legacy transport requirements. A registered endpoint is not proof that the current Ecom build can execute it or that its date and permission semantics have been verified. Use `sycm schema --operation ID --json` before execution. Never substitute another date to make a request succeed.
|
|
24
|
+
|
|
25
|
+
`sycm open --operation ID --container VALUE --json` is the authorized page-preparation branch: background/reuse by default, `--foreground` for explicit visible login/verification. Opening is not proof of authentication; resume the same registered read after preparation. Prefer native initialization and preserve the caller’s page-fallback preference.
|
|
26
|
+
|
|
27
|
+
`sycm run --operation ID --container VALUE --params-json JSON --json` fixes the native instance and container and validates the business envelope. Verify the returned period, platform update time and requested account before reporting metrics. A successful all-zero response is not by itself proof of zero business activity.
|
|
28
|
+
|
|
29
|
+
Mutation plan/apply commands create local records and return reviewed workflow instructions. They perform no website submission. Report `executed=false` and require actual executor and readback evidence before describing a platform change as completed. Existing authorization can cover the reviewed plan; a development request itself grants no real store mutation authority.
|
|
30
|
+
|
|
21
31
|
## Diagnosis and recovery
|
|
22
32
|
|
|
33
|
+
- BROWSER_UPGRADE_REQUIRED: update the Ecom browser and CLI together; native business protocol 1 requires matching compiled registry hashes. Preserve the account session.
|
|
34
|
+
- AUTH_CONTEXT_REQUIRED: keep the existing login; native service context may need adapter diagnosis even when the page is logged in. Report the gap rather than repeatedly requesting login.
|
|
35
|
+
- SERVICE_ADAPTER_REQUIRED: report the missing native adapter and preserve the selected container. Do not switch to CDP or another browser.
|
|
36
|
+
- AUTH_REQUIRED, PERMISSION_REQUIRED or RISK_VERIFICATION_REQUIRED: complete the required first-party action in this original container, then explicitly resume.
|
|
23
37
|
- SHOP_NOT_READY: inspect this exact account. Complete login and platform-identity verification through its Ecom account management page; a cookie expiry or account name does not satisfy readiness. If the native adapter cannot verify identity, report that gap and stop the data request. Return to step 5 only after readiness is established.
|
|
24
38
|
- SHOP_VERIFICATION_FAILED: stop; the browser’s pre-request identity check failed. Preserve the selected target and report the failure.
|
|
25
39
|
- ORIGIN_NOT_ALLOWED, CREDENTIAL_INPUT_REJECTED, METHOD_UNSUPPORTED or INVALID_URL: correct the intended input within the declared contract; do not broaden browser permissions.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"status": "implemented",
|
|
11
11
|
"discovery": "Current host resolves exact Skill directory and Node.js runtime; probe CLI version and capabilities",
|
|
12
|
-
"normalization": "Preserve
|
|
12
|
+
"normalization": "Preserve the complete structured result, including container metadata, instance and operation identity, HTTP status, platform JSON and errors; attach process exit status separately",
|
|
13
13
|
"permissions": "Local execution; explicit selected-target maintenance authorization",
|
|
14
14
|
"limitations": "Browser transport currently macOS only; host discovery evidence is separate",
|
|
15
15
|
"capability_id": "command.execute",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Ecom business capability contract
|
|
2
|
+
|
|
3
|
+
Use this reference for registered reads and mutation planning. Inputs are the operation, exact container, business date/filters and authorized read or local plan intent. Output is structured data with operation/instance/container identity, a precise unsupported/authentication error, or a reviewed local mutation plan. Analysis, commercial decisions and actual submission are separate responsibilities.
|
|
4
|
+
|
|
5
|
+
## Read flow
|
|
6
|
+
|
|
7
|
+
1. Inspect `sycm capabilities --json` and choose the atomic operation. The snapshot includes 124 source registry operations plus 11 category/market/search endpoints from first-class functions. The 37 business descriptions are application workflows, not additional automatic APIs.
|
|
8
|
+
2. Inspect `sycm schema --operation ID --json`. Supply required fields through `--params-json` as an object or JSON file. Preserve parameter spelling and date semantics. Today's realtime data requires a suitable endpoint.
|
|
9
|
+
3. Locate the account with `containers list` and `containers inspect`; select its stable ID and intended instance. Execute `sycm run --operation ID --container ID --params-json JSON --json`, with `--profile` or `--instance` when needed.
|
|
10
|
+
4. Native business protocol 1 covers all 135 compiled reads: 88 SYCM GET endpoints, 30 Alimama POST reads, seven DMP reads and ten coupon reads. The browser prepares service authentication inside the selected persistent container. Browser/CLI catalog hashes must agree. Older Ecom builds can use only the 88 legacy SYCM GET reads; other reads require a browser upgrade. POST describes a registered read protocol, not a mutation.
|
|
11
|
+
5. `businessStatus=success` means the business envelope passed status checks. Verify period, completeness, permissions and update time separately. The browser rechecks bound shop identity before requesting. Individual endpoint live acceptance is separate from registry/transport coverage. An all-zero response alone does not establish zero business activity.
|
|
12
|
+
6. Preserve operation ID and executed status on failure. Inspect unknown outcomes before retry. Login, risk challenges and service authorization are completed manually in the original container. Credentials remain browser-private; no CDP or alternative browser is used.
|
|
13
|
+
|
|
14
|
+
For pagination, pin the first response instance ID and stable container ID in every subsequent call with `--instance` and `--container`; issue explicit page/pageSize reads and compose results using stable IDs. Market pageSize is limited to 20. Record termination and coverage; one page is not a full dataset. Locate categories with `category.tree` or `category.main`. These commands return API data; analysis and reports remain caller-owned.
|
|
15
|
+
|
|
16
|
+
## Mutation-plan flow
|
|
17
|
+
|
|
18
|
+
Seven workflows cover product drafts, migration, search/recommend publishing, video, materials, shop hosting configuration and price plans. Required fields, steps, exclusions and readback criteria are discoverable in `sycm capabilities`.
|
|
19
|
+
|
|
20
|
+
1. Select the verified account and gather the required payload. Check intended changes and existing authorization.
|
|
21
|
+
2. Run `sycm mutation plan --operation CAPABILITY_ID --container ID --params-json JSON --json`. It verifies the account and creates a private 15-minute plan, payload digest, idempotency key and confirmation code. Review the actual payload, target and impact with the user.
|
|
22
|
+
3. `sycm mutation show --plan PLAN_ID --json` returns metadata without the payload or confirmation digest.
|
|
23
|
+
4. Within authorization, run `sycm mutation apply --plan PLAN_ID --container ID --confirm CODE --json`. It rechecks instance/container/platform identity, expiry and integrity, and authorizes the plan once.
|
|
24
|
+
5. The result has `executed=false`, `automaticSubmission=false` and workflow/readback instructions. This preserves the source planning contract. A real executor is a separate capability; report completion only with real submission and readback evidence. Installation/testing requires no real store mutation.
|
|
25
|
+
|
|
26
|
+
Private plan state lives beneath the configured CLI state directory and is excluded from npm. Restarted instances, changed targets, expired/tampered plans, wrong codes and used plans stop authorization. Resolve the cause and review a new plan rather than bypassing checks.
|
|
27
|
+
|
|
28
|
+
## Native service context
|
|
29
|
+
|
|
30
|
+
SYCM uses its own homepage request token; Alimama uses its fixed checkAccess handshake; coupon reads use the container entry page cookie context; DMP uses container cookies and its fixed read-only user-info initialization endpoint, then retains the returned request context inside the browser. Website account creation and account switching remain user/platform actions. Native data requests do not create or activate tabs. Credentials are not passed through the CLI.
|
|
31
|
+
|
|
32
|
+
Alimama dynamic modules can return templates. The browser expands only data-flow IDs returned by that registered module, through a fixed read endpoint, with a maximum of 16 flows and the same total timeout. The CLI returns the original redacted `data`, optional `dataFlows`, and `resolvedResponse` when expansion is needed. Failed flows are errors. Successful flows with missing template values return `businessStatus=partial` and `completeness.complete=false`; do not treat a template or missing value as business data. Missing browser expansion returns DATA_FLOW_EXPANSION_REQUIRED. Plain member configuration reads may need an explicit `bizCode` such as the service's `universalBP`; inspect business semantics rather than assuming empty parameters will succeed.
|
|
33
|
+
|
|
34
|
+
`AUTH_CONTEXT_REQUIRED` can indicate an adapter-context gap even when the visible service is logged in. Preserve the existing login and report the exact failure; repeated user login is not the default recovery. `BROWSER_UPGRADE_REQUIRED` requires matching browser and CLI catalogs. Per-endpoint business permission and actual dates remain separate acceptance checks. Read parameter JSON is limited to 24 KiB; results to 2 MiB; the client waits up to 75 seconds.
|
|
35
|
+
|
|
36
|
+
## Authorized page fallback
|
|
37
|
+
|
|
38
|
+
Prefer native reads and native service initialization. If an entry must prepare a service session and the caller authorizes page fallback, use `sycm open --operation ID --container ID --instance INSTANCE --json`. It selects the compiled first-party entry, preserves the account homepage and container ID, and opens/reuses a background container tab. `--foreground` explicitly opens a foreground tab when the user needs to log in or verify. No arbitrary URL, browser profile or JavaScript input is accepted by this command.
|
|
39
|
+
|
|
40
|
+
A navigation result is not a login check. Preserve the original target and let the platform finish loading; the user handles any login, authorization or CAPTCHA. Resume the registered read only after the required preparation. Do not use a different container or copy credentials. Once a valid service session is established, native requests continue without routine page opening. An HTTP/context failure does not automatically prove expiration: inspect the bounded phase/network diagnostics before requesting another login.
|