cyymall-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/bin/cyy.js +2 -0
- package/package.json +33 -0
- package/src/biz.js +14 -0
- package/src/cli.js +208 -0
- package/src/commands/apiCall.js +149 -0
- package/src/commands/auth.js +134 -0
- package/src/commands/cart.js +55 -0
- package/src/commands/order.js +327 -0
- package/src/commands/product.js +56 -0
- package/src/commands/serve.js +84 -0
- package/src/commands/shop.js +287 -0
- package/src/config.js +82 -0
- package/src/http.js +94 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# cyymall-cli (`cyy`)
|
|
2
|
+
|
|
3
|
+
Node.js CLI for CyyMall / 菜洋洋商城 APIs. Spec: [`../app-api-cli-spec.md`](../app-api-cli-spec.md).
|
|
4
|
+
|
|
5
|
+
## Install from npm
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g cyymall-cli
|
|
9
|
+
cyy --help
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Package: [cyymall-cli](https://www.npmjs.com/package/cyymall-cli)(发布后可用;首次发布前请用下方「本地开发」安装。)
|
|
13
|
+
|
|
14
|
+
## Install (local dev)
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cd cyymall-cli
|
|
18
|
+
npm install
|
|
19
|
+
npm link
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then run `cyy --help`.
|
|
23
|
+
|
|
24
|
+
## Publish to npm (maintainers)
|
|
25
|
+
|
|
26
|
+
1. 在 [npmjs.com](https://www.npmjs.com/) 登录;发布策略若要求 **2FA**,必须先完成账号安全设置。
|
|
27
|
+
2. 本机认证二选一(**不要把 token 写进仓库或发给他人**):
|
|
28
|
+
- **`npm login`**(交互式);或
|
|
29
|
+
- **`npm config set //registry.npmjs.org/:_authToken=<token>`**(仅本机 `~/.npmrc`,勿提交 Git)。
|
|
30
|
+
3. 在项目目录发布:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cd cyymall-cli
|
|
34
|
+
npm publish
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
若账号启用了 **2FA 且发布时必须验证**,仅配置 `_authToken` 仍可能 **403**,需带一次性密码:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm publish --otp=123456
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
或使用 npm 后台生成的 **Granular Access Token**:权限包含 **Publish**,并在创建时勾选 **Bypass two-factor authentication (2FA)**(仅在你信任该 token 存放环境时使用;详见 npm 创建 token 页面说明)。
|
|
44
|
+
|
|
45
|
+
4. 若出现 **`403 Forbidden … Two-factor authentication or granular access token with bypass 2fa`**:按上一步处理(`--otp` 或带 bypass 的 granular token),**不是** `package.json` 或 tarball 的问题。
|
|
46
|
+
5. 后续发版:修改 **`package.json` 的 `version`**,再执行 `npm publish`(必要时同样加 `--otp`)。
|
|
47
|
+
|
|
48
|
+
`npm pack --dry-run` 可预览将要上传的文件列表(由 **`files`** 字段控制)。
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- Node.js >= 18
|
|
53
|
+
|
|
54
|
+
## Quick commands
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cyy config path
|
|
58
|
+
cyy auth login --phone <mobile> --password <pwd>
|
|
59
|
+
cyy auth whoami
|
|
60
|
+
cyy shop list
|
|
61
|
+
cyy shop sites --shop-id <门店ID>
|
|
62
|
+
cyy shop use --shop-id <门店ID>
|
|
63
|
+
cyy shop use-site --site-id <站点ID>
|
|
64
|
+
cyy product search --keyword 牛奶
|
|
65
|
+
cyy api call --method POST --module PRODUCT --path /app/product/getSkuList --body-file body.json
|
|
66
|
+
cyy order quick --keyword 牛奶 --quantity 1 --unit 袋
|
|
67
|
+
cyy serve --port 8787
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Session vs bootstrap env
|
|
71
|
+
|
|
72
|
+
**After `cyy auth login` succeeds**, the CLI reads the session token from **`data.token`** or **`data.appToken`** (plus optional **`shopId` / `siteId` / `versionCode` / `loginId`→`memberId`** when present) and saves them under **`~/.cyymall/config.json`**. If the API omits `shopId`/`siteId`, previously saved or bootstrap values are kept where applicable.
|
|
73
|
+
后续 **`api call`、`product search`、`order quick`** 等命令都会从该文件加载并组装 Header —— **正常情况下不再需要设置 `CYY_BOOTSTRAP_*`**,等价于「登录接口返回什么就持久化什么,进程退出后仍可用」(磁盘配置,不是仅限内存)。
|
|
74
|
+
|
|
75
|
+
**`CYY_BOOTSTRAP_*` 仅用于「尚未登录」时**(例如第一次调用 `/app/auth/ua/registerMember/v2` 本身需要的网关 Header)。若你的环境里登录接口在未带 shop/token 时会被网关拒绝,再按需配置这些变量;**登录成功后的业务请求一律优先使用配置文件里的会话字段**。
|
|
76
|
+
|
|
77
|
+
| Variable | Purpose |
|
|
78
|
+
|----------|---------|
|
|
79
|
+
| `CYY_BASE_URL` | Override API host (default `https://dhcmall.ifoodbuy.com`) |
|
|
80
|
+
| `CYY_PASSWORD` | Used by `auth login` if `--password` omitted |
|
|
81
|
+
| `CYY_BOOTSTRAP_TOKEN` | Optional — **only before login**, gateway may expect placeholder token |
|
|
82
|
+
| `CYY_BOOTSTRAP_SHOP_ID` | Optional — **only before login** |
|
|
83
|
+
| `CYY_BOOTSTRAP_SITE_ID` | Default `1` when bootstrapping |
|
|
84
|
+
| `CYY_BOOTSTRAP_MEMBER_ID` | Optional — **only before login** |
|
|
85
|
+
| `CYY_BOOTSTRAP_VERSION_CODE` | App version header when bootstrapping / fallback (`22118`) |
|
|
86
|
+
|
|
87
|
+
权威规格(含请求体附录)建议以 Android 工程内的 **`docs/app-api-cli-spec.md`** 为准;若与本仓库根目录同名文档不一致,以该版本为准。
|
|
88
|
+
|
|
89
|
+
## Implementation phases
|
|
90
|
+
|
|
91
|
+
See [`../README_CLI.md`](../README_CLI.md) for staged rollout and acceptance notes.
|
package/bin/cyy.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cyymall-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CyyMall / 菜洋洋商城 API CLI (per app-api-cli-spec)",
|
|
5
|
+
"bin": {
|
|
6
|
+
"cyy": "bin/cyy.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "src/cli.js",
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
"README.md",
|
|
14
|
+
"package.json"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "node bin/cyy.js",
|
|
21
|
+
"lint": "node --check src/cli.js",
|
|
22
|
+
"prepublishOnly": "node --check src/cli.js && node --check bin/cyy.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"cyymall",
|
|
26
|
+
"cli",
|
|
27
|
+
"dhcmall"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^12.1.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/biz.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {unknown} upstream
|
|
5
|
+
*/
|
|
6
|
+
function isBizSuccess(upstream) {
|
|
7
|
+
if (upstream == null || typeof upstream !== "object") return false;
|
|
8
|
+
const o = /** @type {{success?:boolean,code?:unknown}} */ (upstream);
|
|
9
|
+
if (o.success === true) return true;
|
|
10
|
+
if (o.code === "00000" || o.code === 0 || o.code === "0") return true;
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { isBizSuccess };
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const pkg = require(path.join(__dirname, "..", "package.json"));
|
|
5
|
+
|
|
6
|
+
const config = require("./config");
|
|
7
|
+
const apiCall = require("./commands/apiCall");
|
|
8
|
+
const auth = require("./commands/auth");
|
|
9
|
+
const product = require("./commands/product");
|
|
10
|
+
const cart = require("./commands/cart");
|
|
11
|
+
const order = require("./commands/order");
|
|
12
|
+
const serve = require("./commands/serve");
|
|
13
|
+
const shop = require("./commands/shop");
|
|
14
|
+
|
|
15
|
+
const { Command } = require("commander");
|
|
16
|
+
const program = new Command();
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name("cyy")
|
|
20
|
+
.description("CyyMall / 菜洋洋商城 CLI (see app-api-cli-spec.md)")
|
|
21
|
+
.version(pkg.version);
|
|
22
|
+
|
|
23
|
+
const cfgCmd = program.command("config").description("Configuration");
|
|
24
|
+
|
|
25
|
+
cfgCmd
|
|
26
|
+
.command("path")
|
|
27
|
+
.description("Print config file path")
|
|
28
|
+
.action(() => {
|
|
29
|
+
console.log(config.getConfigPath());
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
cfgCmd
|
|
33
|
+
.command("show")
|
|
34
|
+
.description("Show config (token masked)")
|
|
35
|
+
.action(() => {
|
|
36
|
+
const c = config.loadConfig();
|
|
37
|
+
if (!c) {
|
|
38
|
+
console.log(JSON.stringify({ loggedIn: false }, null, 2));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const masked = { ...c, token: config.maskToken(String(c.token || "")) };
|
|
42
|
+
console.log(JSON.stringify(masked, null, 2));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function collect(value, previous) {
|
|
46
|
+
return previous.concat([value]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const api = program.command("api").description("Low-level API calls");
|
|
50
|
+
|
|
51
|
+
api
|
|
52
|
+
.command("call")
|
|
53
|
+
.description("Invoke any mapped endpoint (spec §3)")
|
|
54
|
+
.requiredOption("--method <verb>", "HTTP method: GET, POST, PUT, PATCH, DELETE")
|
|
55
|
+
.requiredOption("--module <name>", "DEFAULT | ORDER | PRODUCT | PLATFORM | PAYMENT | OSS | BIZ")
|
|
56
|
+
.requiredOption("--path <path>", "Path starting with /, e.g. /app/product/getSkuList")
|
|
57
|
+
.option("--body-json <json>", "JSON body string")
|
|
58
|
+
.option("--body-file <file>", "Read JSON body from UTF-8 file")
|
|
59
|
+
.option("--query <pair>", "Repeatable: key=value for query string", collect, [])
|
|
60
|
+
.option("--header <h>", 'Repeatable: "Name: value"', collect, [])
|
|
61
|
+
.option("--bare", "Bootstrap headers only (do not merge saved session)")
|
|
62
|
+
.action(async (opts) => {
|
|
63
|
+
await apiCall.runApiCall({
|
|
64
|
+
...opts,
|
|
65
|
+
noAuth: Boolean(opts.bare),
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const authCmd = program.command("auth").description("Authentication");
|
|
70
|
+
|
|
71
|
+
authCmd
|
|
72
|
+
.command("login")
|
|
73
|
+
.description("Password login (SHA-256); saves ~/.cyymall/config.json")
|
|
74
|
+
.requiredOption("--phone <p>", "Mobile phone")
|
|
75
|
+
.option("--password <pwd>", "Plain password (or set env CYY_PASSWORD)")
|
|
76
|
+
.action(async (opts) => {
|
|
77
|
+
const pwd = opts.password || process.env.CYY_PASSWORD;
|
|
78
|
+
if (!pwd) {
|
|
79
|
+
console.error("cyy: provide --password or set CYY_PASSWORD");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
await auth.login(opts.phone, pwd);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
authCmd
|
|
86
|
+
.command("whoami")
|
|
87
|
+
.description("GET /member/getInfo/V2 using saved token")
|
|
88
|
+
.action(async () => {
|
|
89
|
+
await auth.whoami();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const prod = program.command("product").description("Product helpers");
|
|
93
|
+
|
|
94
|
+
prod
|
|
95
|
+
.command("search")
|
|
96
|
+
.description("POST /mall-product/app/product/getSkuList")
|
|
97
|
+
.requiredOption("--keyword <k>", "spuName keyword")
|
|
98
|
+
.option("--shop-id <id>", "Override shopId (default from session)")
|
|
99
|
+
.option("--site-id <id>", "Override siteId")
|
|
100
|
+
.option("--page <n>", "pageNum", "1")
|
|
101
|
+
.option("--page-size <n>", "pageSize", "10")
|
|
102
|
+
.option("--stock-flag <s>", "stockFlag string (Java SearchCommodityReq)", "0")
|
|
103
|
+
.action(async (opts) => {
|
|
104
|
+
await product.search(opts);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const shopCmd = program.command("shop").description("Member shops and delivery sites");
|
|
108
|
+
|
|
109
|
+
shopCmd
|
|
110
|
+
.command("list")
|
|
111
|
+
.description("POST /shop/member/list — paginated shops for current member (GetShopReq)")
|
|
112
|
+
.option("--page <n>", "pageNum", "1")
|
|
113
|
+
.option("--page-size <n>", "pageSize", "20")
|
|
114
|
+
.option("--name <s>", "shopName filter", "")
|
|
115
|
+
.option("--object-code <n>", "objectCode", "3")
|
|
116
|
+
.action(async (opts) => {
|
|
117
|
+
await shop.list(opts);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
shopCmd
|
|
121
|
+
.command("sites")
|
|
122
|
+
.description("GET /shop/member/store/list — sites for a shop")
|
|
123
|
+
.option("--shop-id <id>", "Shop id (default: session shop_id)")
|
|
124
|
+
.option("--site-name <s>", "Optional siteName filter")
|
|
125
|
+
.action(async (opts) => {
|
|
126
|
+
await shop.sites(opts);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
shopCmd
|
|
130
|
+
.command("use")
|
|
131
|
+
.description("Set default shop_id only in ~/.cyymall/config.json (site_id unchanged; validate via shop list)")
|
|
132
|
+
.requiredOption("--shop-id <id>", "Numeric shop id from cyy shop list")
|
|
133
|
+
.action(async (opts) => {
|
|
134
|
+
await shop.useShop(opts);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
shopCmd
|
|
138
|
+
.command("use-site")
|
|
139
|
+
.description("Set default site_id for current session shop_id (validates against shop sites)")
|
|
140
|
+
.requiredOption("--site-id <id>", "Site id from shop sites")
|
|
141
|
+
.action(async (opts) => {
|
|
142
|
+
await shop.useSite(opts);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const cartCmd = program.command("cart").description("Shopping cart");
|
|
146
|
+
|
|
147
|
+
cartCmd
|
|
148
|
+
.command("add")
|
|
149
|
+
.description("POST /mall-order/app/order/cart")
|
|
150
|
+
.option("--body-file <file>", "Cart JSON body")
|
|
151
|
+
.option("--body-json <json>", "Cart JSON string")
|
|
152
|
+
.action(async (opts) => {
|
|
153
|
+
await cart.add(opts);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const orderCmd = program.command("order").description("Order flow");
|
|
157
|
+
|
|
158
|
+
orderCmd
|
|
159
|
+
.command("pre-settle")
|
|
160
|
+
.description("POST /mall-order/app/order/preSettleOrder")
|
|
161
|
+
.option("--body-file <file>", "")
|
|
162
|
+
.option("--body-json <json>", "")
|
|
163
|
+
.action(async (opts) => {
|
|
164
|
+
await order.preSettle(opts);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
orderCmd
|
|
168
|
+
.command("confirm")
|
|
169
|
+
.description("POST /mall-order/app/order/confirmOrder")
|
|
170
|
+
.option("--body-file <file>", "")
|
|
171
|
+
.option("--body-json <json>", "")
|
|
172
|
+
.action(async (opts) => {
|
|
173
|
+
await order.confirm(opts);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
orderCmd
|
|
177
|
+
.command("pay-url")
|
|
178
|
+
.description("Build H5 replacePay URL for order")
|
|
179
|
+
.requiredOption("--order-id <id>", "Order id from confirm response")
|
|
180
|
+
.action(async (opts) => {
|
|
181
|
+
await order.payUrl(opts);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
orderCmd
|
|
185
|
+
.command("quick")
|
|
186
|
+
.description("Search → cart → pre-settle → confirm → pay URL (first search hit)")
|
|
187
|
+
.requiredOption("--keyword <k>", "Product keyword")
|
|
188
|
+
.option("--quantity <n>", "Qty", "1")
|
|
189
|
+
.option("--unit <u>", "袋 | 提 | 箱", "袋")
|
|
190
|
+
.option("--shop-id <id>", "shopId override")
|
|
191
|
+
.option("--site-id <id>", "siteId override")
|
|
192
|
+
.action(async (opts) => {
|
|
193
|
+
await order.quick(opts);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
program
|
|
197
|
+
.command("serve")
|
|
198
|
+
.description("Local HTTP shim: POST /invoke JSON body { method, module, path, body?, query?, noAuth? }")
|
|
199
|
+
.option("-p, --port <n>", "port", "8787")
|
|
200
|
+
.option("--host <h>", "bind address", "127.0.0.1")
|
|
201
|
+
.action((opts) => {
|
|
202
|
+
serve.runServe({ port: opts.port, host: opts.host });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
program.parseAsync(process.argv).catch((e) => {
|
|
206
|
+
console.error(e);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const config = require("../config");
|
|
4
|
+
const http = require("../http");
|
|
5
|
+
const biz = require("../biz");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse repeated --query key=value
|
|
9
|
+
* @param {string[]} queryArgs
|
|
10
|
+
*/
|
|
11
|
+
function parseQueryPairs(queryArgs) {
|
|
12
|
+
const q = {};
|
|
13
|
+
if (!queryArgs || !queryArgs.length) return q;
|
|
14
|
+
for (const pair of queryArgs) {
|
|
15
|
+
const i = pair.indexOf("=");
|
|
16
|
+
if (i <= 0) continue;
|
|
17
|
+
const k = pair.slice(0, i).trim();
|
|
18
|
+
const v = pair.slice(i + 1);
|
|
19
|
+
q[k] = v;
|
|
20
|
+
}
|
|
21
|
+
return q;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function appendQuery(url, queryObj) {
|
|
25
|
+
const keys = Object.keys(queryObj);
|
|
26
|
+
if (!keys.length) return url;
|
|
27
|
+
const u = new URL(url);
|
|
28
|
+
for (const k of keys) {
|
|
29
|
+
u.searchParams.set(k, queryObj[k]);
|
|
30
|
+
}
|
|
31
|
+
return u.toString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Shared executor for CLI and HTTP serve mode.
|
|
36
|
+
* @param {object} opts
|
|
37
|
+
*/
|
|
38
|
+
async function executeApiCall(opts) {
|
|
39
|
+
const {
|
|
40
|
+
method,
|
|
41
|
+
module: moduleKey,
|
|
42
|
+
path: pathSuffix,
|
|
43
|
+
bodyJson,
|
|
44
|
+
bodyFile,
|
|
45
|
+
bodyObj,
|
|
46
|
+
query: queryArgs,
|
|
47
|
+
queryObj: queryObjIn,
|
|
48
|
+
noAuth,
|
|
49
|
+
header: headerPairs,
|
|
50
|
+
} = opts;
|
|
51
|
+
|
|
52
|
+
let bodyStr = null;
|
|
53
|
+
if (bodyFile) {
|
|
54
|
+
const fs = require("fs");
|
|
55
|
+
bodyStr = fs.readFileSync(bodyFile, "utf8");
|
|
56
|
+
} else if (bodyObj != null) {
|
|
57
|
+
bodyStr = JSON.stringify(bodyObj);
|
|
58
|
+
} else if (bodyJson != null && bodyJson !== "") {
|
|
59
|
+
bodyStr = bodyJson;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (bodyStr && method === "GET") {
|
|
63
|
+
return { error: "GET request should not use body; use query only.", exitCode: 1 };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let url = http.moduleUrl(moduleKey, pathSuffix);
|
|
67
|
+
const queryObj =
|
|
68
|
+
queryObjIn && typeof queryObjIn === "object"
|
|
69
|
+
? queryObjIn
|
|
70
|
+
: parseQueryPairs(queryArgs || []);
|
|
71
|
+
url = appendQuery(url, queryObj);
|
|
72
|
+
|
|
73
|
+
/** @type {Record<string,string>} */
|
|
74
|
+
let headers = {};
|
|
75
|
+
|
|
76
|
+
if (!noAuth) {
|
|
77
|
+
const cfg = config.loadConfig();
|
|
78
|
+
headers = /** @type {Record<string,string>} */ (
|
|
79
|
+
http.buildAuthHeaders(cfg)
|
|
80
|
+
);
|
|
81
|
+
} else {
|
|
82
|
+
headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(null));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const extras = headerPairs || [];
|
|
86
|
+
for (const h of extras) {
|
|
87
|
+
const idx = h.indexOf(":");
|
|
88
|
+
if (idx > 0) {
|
|
89
|
+
const hk = h.slice(0, idx).trim();
|
|
90
|
+
const hv = h.slice(idx + 1).trim();
|
|
91
|
+
headers[hk] = hv;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (method === "GET" || method === "HEAD") {
|
|
96
|
+
delete headers["Content-Type"];
|
|
97
|
+
} else if (bodyStr) {
|
|
98
|
+
headers["Content-Type"] = headers["Content-Type"] || "application/json;charset=UTF-8";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { ok, status, json } = await http.request(url, {
|
|
102
|
+
method,
|
|
103
|
+
headers,
|
|
104
|
+
body: bodyStr,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const bizOk = ok && (json == null || biz.isBizSuccess(json));
|
|
108
|
+
const envelopeSuccess = bizOk;
|
|
109
|
+
const traceId = `local-${require("crypto").randomBytes(8).toString("hex")}`;
|
|
110
|
+
const message =
|
|
111
|
+
json && typeof json === "object" && "msg" in json
|
|
112
|
+
? String(/** @type {{msg?:string}} */ (json).msg)
|
|
113
|
+
: ok
|
|
114
|
+
? "success"
|
|
115
|
+
: `HTTP ${status}`;
|
|
116
|
+
|
|
117
|
+
const envelope = {
|
|
118
|
+
success: envelopeSuccess,
|
|
119
|
+
code: envelopeSuccess ? "OK" : "UPSTREAM_ERROR",
|
|
120
|
+
message,
|
|
121
|
+
data: { upstream: json, httpStatus: status },
|
|
122
|
+
traceId,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return { envelope, exitCode: envelopeSuccess ? 0 : 2 };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {import('commander').OptionValues} opts
|
|
130
|
+
*/
|
|
131
|
+
async function runApiCall(opts) {
|
|
132
|
+
if (opts.bodyJson && opts.method === "GET") {
|
|
133
|
+
console.error("cyy: GET request should not use body; use --query instead.");
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
const merged = {
|
|
137
|
+
...opts,
|
|
138
|
+
noAuth: Boolean(opts.noAuth ?? opts.bare),
|
|
139
|
+
};
|
|
140
|
+
const result = await executeApiCall(merged);
|
|
141
|
+
if (result.error) {
|
|
142
|
+
console.error(`cyy: ${result.error}`);
|
|
143
|
+
process.exit(result.exitCode ?? 1);
|
|
144
|
+
}
|
|
145
|
+
console.log(JSON.stringify(result.envelope, null, 2));
|
|
146
|
+
process.exit(result.exitCode ?? 1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { runApiCall, executeApiCall, parseQueryPairs };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const config = require("../config");
|
|
5
|
+
const http = require("../http");
|
|
6
|
+
const biz = require("../biz");
|
|
7
|
+
|
|
8
|
+
function sha256Hex(s) {
|
|
9
|
+
return crypto.createHash("sha256").update(s, "utf8").digest("hex");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Login `data` may use `token` (Android LoginBean / Gson), `appToken`, or `accessToken`.
|
|
14
|
+
* @param {Record<string, unknown>|null|undefined} data
|
|
15
|
+
*/
|
|
16
|
+
function pickLoginToken(data) {
|
|
17
|
+
if (!data || typeof data !== "object") return "";
|
|
18
|
+
const d = /** @type {Record<string, unknown>} */ (data);
|
|
19
|
+
const v = d.token ?? d.appToken ?? d.accessToken;
|
|
20
|
+
return v != null ? String(v) : "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function login(phone, password) {
|
|
24
|
+
const url = http.moduleUrl("DEFAULT", "/app/auth/ua/registerMember/v2");
|
|
25
|
+
const headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(null));
|
|
26
|
+
const body = JSON.stringify({
|
|
27
|
+
phone,
|
|
28
|
+
objectCode: "3",
|
|
29
|
+
password: sha256Hex(password),
|
|
30
|
+
passwordLoginFlag: true,
|
|
31
|
+
onKeyLoginFlag: false,
|
|
32
|
+
wxOpenid: "",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const { ok, json } = await http.request(url, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers,
|
|
38
|
+
body,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!ok || !biz.isBizSuccess(json)) {
|
|
42
|
+
const msg =
|
|
43
|
+
json && typeof json === "object" && "msg" in json
|
|
44
|
+
? String(/** @type {{msg?:string}} */ (json).msg)
|
|
45
|
+
: "login failed";
|
|
46
|
+
console.error(`cyy: ${msg}`);
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const raw = /** @type {{data?:Record<string,unknown>|null}} */ (json).data;
|
|
51
|
+
const data = raw && typeof raw === "object" ? raw : {};
|
|
52
|
+
const prev = config.loadConfig() || {};
|
|
53
|
+
|
|
54
|
+
const loginId = String(data.loginId || "");
|
|
55
|
+
const memberId = loginId.includes("_") ? loginId.split("_")[0] : loginId;
|
|
56
|
+
|
|
57
|
+
const token = pickLoginToken(data);
|
|
58
|
+
if (!token) {
|
|
59
|
+
console.error(
|
|
60
|
+
"cyy: login reported success but data has no token (expected token, appToken, or accessToken)",
|
|
61
|
+
);
|
|
62
|
+
process.exit(2);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const saved = {
|
|
66
|
+
phone,
|
|
67
|
+
token,
|
|
68
|
+
member_id: memberId || String(prev.member_id || ""),
|
|
69
|
+
shop_id: data.shopId ?? prev.shop_id,
|
|
70
|
+
site_id: data.siteId ?? prev.site_id ?? "1",
|
|
71
|
+
version_code: String(data.versionCode ?? prev.version_code ?? http.DEFAULT_VERSION),
|
|
72
|
+
login_time: Math.floor(Date.now() / 1000),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
config.saveConfig(saved);
|
|
76
|
+
|
|
77
|
+
const traceId = `local-${crypto.randomBytes(8).toString("hex")}`;
|
|
78
|
+
console.log(
|
|
79
|
+
JSON.stringify(
|
|
80
|
+
{
|
|
81
|
+
success: true,
|
|
82
|
+
code: "OK",
|
|
83
|
+
message: "login success",
|
|
84
|
+
data: {
|
|
85
|
+
phone: saved.phone,
|
|
86
|
+
member_id: saved.member_id,
|
|
87
|
+
shop_id: saved.shop_id,
|
|
88
|
+
site_id: saved.site_id,
|
|
89
|
+
version_code: saved.version_code,
|
|
90
|
+
token_preview: config.maskToken(saved.token),
|
|
91
|
+
},
|
|
92
|
+
traceId,
|
|
93
|
+
},
|
|
94
|
+
null,
|
|
95
|
+
2,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function whoami() {
|
|
102
|
+
const cfg = config.loadConfig();
|
|
103
|
+
if (!cfg?.token) {
|
|
104
|
+
console.error("cyy: not logged in. Run: cyy auth login --phone ... --password ...");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const url = http.moduleUrl("DEFAULT", "/member/getInfo/V2");
|
|
108
|
+
const headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(cfg));
|
|
109
|
+
delete headers["Content-Type"];
|
|
110
|
+
const { ok, json } = await http.request(url, {
|
|
111
|
+
method: "GET",
|
|
112
|
+
headers,
|
|
113
|
+
body: null,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const traceId = `local-${crypto.randomBytes(8).toString("hex")}`;
|
|
117
|
+
const success = ok && biz.isBizSuccess(json);
|
|
118
|
+
console.log(
|
|
119
|
+
JSON.stringify(
|
|
120
|
+
{
|
|
121
|
+
success,
|
|
122
|
+
code: success ? "OK" : "UPSTREAM_ERROR",
|
|
123
|
+
message: success ? "success" : "whoami failed",
|
|
124
|
+
data: { upstream: json },
|
|
125
|
+
traceId,
|
|
126
|
+
},
|
|
127
|
+
null,
|
|
128
|
+
2,
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
process.exit(success ? 0 : 2);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { login, whoami };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
const http = require("../http");
|
|
6
|
+
const config = require("../config");
|
|
7
|
+
const biz = require("../biz");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ bodyFile?: string, bodyJson?: string }} opts
|
|
11
|
+
*/
|
|
12
|
+
async function add(opts) {
|
|
13
|
+
const cfg = config.loadConfig();
|
|
14
|
+
if (!cfg?.token) {
|
|
15
|
+
console.error("cyy: not logged in.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let raw = opts.bodyJson;
|
|
20
|
+
if (opts.bodyFile) {
|
|
21
|
+
raw = fs.readFileSync(opts.bodyFile, "utf8");
|
|
22
|
+
}
|
|
23
|
+
if (!raw) {
|
|
24
|
+
console.error("cyy: provide --body-file or --body-json");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const url = http.moduleUrl("ORDER", "/app/order/cart");
|
|
29
|
+
const headers = /** @type {Record<string,string>} */ (http.buildAuthHeaders(cfg));
|
|
30
|
+
|
|
31
|
+
const { ok, json } = await http.request(url, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers,
|
|
34
|
+
body: raw,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const traceId = `local-${crypto.randomBytes(8).toString("hex")}`;
|
|
38
|
+
const success = ok && biz.isBizSuccess(json);
|
|
39
|
+
console.log(
|
|
40
|
+
JSON.stringify(
|
|
41
|
+
{
|
|
42
|
+
success,
|
|
43
|
+
code: success ? "OK" : "UPSTREAM_ERROR",
|
|
44
|
+
message: success ? "success" : "cart failed",
|
|
45
|
+
data: { upstream: json },
|
|
46
|
+
traceId,
|
|
47
|
+
},
|
|
48
|
+
null,
|
|
49
|
+
2,
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
process.exit(success ? 0 : 2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { add };
|