byteplan-cli 1.0.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 +75 -0
- package/package.json +43 -0
- package/src/api.js +329 -0
- package/src/cli.js +280 -0
- package/src/commands/index.js +426 -0
- package/src/index.js +46 -0
- package/src/lib/api.js +347 -0
- package/src/lib/config.js +70 -0
- package/src/lib/crypto.js +22 -0
- package/src/lib/output.js +5 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# byteplan-cli
|
|
2
|
+
|
|
3
|
+
BytePlan CLI - Command line tool for BytePlan API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g byteplan-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Login
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
byteplan login -u <phone> -p <password>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### User Commands
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Get user info
|
|
23
|
+
byteplan user info
|
|
24
|
+
|
|
25
|
+
# List available tenants
|
|
26
|
+
byteplan user tenants
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Model Commands
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# List models
|
|
33
|
+
byteplan model list
|
|
34
|
+
|
|
35
|
+
# Get model columns
|
|
36
|
+
byteplan model columns <modelCode>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Data Commands
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Query data
|
|
43
|
+
byteplan data query <modelCode>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Dimension & LOV Commands
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Get dimension values
|
|
50
|
+
byteplan dim <dimCode>
|
|
51
|
+
|
|
52
|
+
# Get LOV values
|
|
53
|
+
byteplan lov <lovCode>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Options
|
|
57
|
+
|
|
58
|
+
- `-e, --env <environment>` - Environment (uat, prod), default: uat
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
Credentials are stored in `~/.byteplan/.env`:
|
|
63
|
+
|
|
64
|
+
```env
|
|
65
|
+
BP_ENV=uat
|
|
66
|
+
BP_USER=<phone>
|
|
67
|
+
BP_PASSWORD=<password>
|
|
68
|
+
ACCESS_TOKEN=<token>
|
|
69
|
+
REFRESH_TOKEN=<refresh_token>
|
|
70
|
+
TOKEN_EXPIRES_IN=<timestamp>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "byteplan-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "BytePlan CLI - Command line tool for BytePlan API",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"byteplan",
|
|
7
|
+
"cli",
|
|
8
|
+
"data-platform"
|
|
9
|
+
],
|
|
10
|
+
"author": "dbfu",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "src/index.js",
|
|
14
|
+
"bin": {
|
|
15
|
+
"byteplan": "./src/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src/",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "echo \"No tests yet\""
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"commander": "^12.0.0",
|
|
26
|
+
"chalk": "^5.3.0",
|
|
27
|
+
"dotenv": "^16.4.0",
|
|
28
|
+
"ora": "^8.0.0",
|
|
29
|
+
"conf": "^12.0.0",
|
|
30
|
+
"node-rsa": "^1.1.1"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/dbfu/byteplan-cli.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/dbfu/byteplan-cli/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/dbfu/byteplan-cli#readme"
|
|
43
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BytePlan API Client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import Conf from 'conf';
|
|
6
|
+
import dotenv from 'dotenv';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
10
|
+
|
|
11
|
+
const configDir = join(homedir(), '.byteplan');
|
|
12
|
+
const envFile = join(configDir, '.env');
|
|
13
|
+
|
|
14
|
+
// Ensure config directory exists
|
|
15
|
+
if (!existsSync(configDir)) {
|
|
16
|
+
mkdirSync(configDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Load .env if exists
|
|
20
|
+
if (existsSync(envFile)) {
|
|
21
|
+
dotenv.config({ path: envFile });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ENVIRONMENTS = {
|
|
25
|
+
uat: 'https://uatapp.byteplan.com',
|
|
26
|
+
prod: 'https://byteplan.51fubei.com',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const BASIC_AUTH = 'Basic UEM6bkxDbndkSWhpeldieWtIeXVaTTZUcFFEZDdLd0s5SVhESzhMR3NhN1NPVw==';
|
|
30
|
+
|
|
31
|
+
const DEFAULT_MENU_HEADERS = {
|
|
32
|
+
'x-menu-code': 'AI_REPORT',
|
|
33
|
+
'x-menu-id': '2008425412219936770',
|
|
34
|
+
'x-menu-params': 'null',
|
|
35
|
+
'x-page-code': 'ai_report',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let currentEnv = process.env.BP_ENV || 'uat';
|
|
39
|
+
let currentToken = process.env.ACCESS_TOKEN || null;
|
|
40
|
+
|
|
41
|
+
export function setEnvironment(env) {
|
|
42
|
+
if (ENVIRONMENTS[env]) {
|
|
43
|
+
currentEnv = env;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getEnvironment() {
|
|
48
|
+
return currentEnv;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getBaseUrl() {
|
|
52
|
+
return ENVIRONMENTS[currentEnv];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function setToken(token) {
|
|
56
|
+
currentToken = token;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getToken() {
|
|
60
|
+
return currentToken;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// RSA 加密
|
|
64
|
+
async function encryptPassword(password, publicKeyBase64) {
|
|
65
|
+
const NodeRSA = (await import('node-rsa')).default;
|
|
66
|
+
|
|
67
|
+
const key = new NodeRSA();
|
|
68
|
+
const publicKeyDer = Buffer.from(publicKeyBase64, 'base64');
|
|
69
|
+
key.importKey(publicKeyDer, 'pkcs8-public-der');
|
|
70
|
+
key.setOptions({ encryptionScheme: 'pkcs1' });
|
|
71
|
+
|
|
72
|
+
return key.encrypt(password, 'base64');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 获取公钥
|
|
76
|
+
export async function getPublicKey() {
|
|
77
|
+
const baseUrl = getBaseUrl();
|
|
78
|
+
const response = await fetch(baseUrl + '/base/util/get/publicKey');
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error('Failed to get public key: ' + response.status);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return response.json();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 保存凭证到 .env
|
|
88
|
+
function saveCredentials(data) {
|
|
89
|
+
const envPath = join(homedir(), '.byteplan', '.env');
|
|
90
|
+
|
|
91
|
+
const lines = [];
|
|
92
|
+
lines.push('BP_ENV=' + currentEnv);
|
|
93
|
+
lines.push('BP_USER=' + data.username);
|
|
94
|
+
lines.push('BP_PASSWORD=' + data.password);
|
|
95
|
+
if (data.accessToken) {
|
|
96
|
+
lines.push('ACCESS_TOKEN=' + data.accessToken);
|
|
97
|
+
}
|
|
98
|
+
if (data.refreshToken) {
|
|
99
|
+
lines.push('REFRESH_TOKEN=' + data.refreshToken);
|
|
100
|
+
}
|
|
101
|
+
if (data.expiresIn) {
|
|
102
|
+
lines.push('TOKEN_EXPIRES_IN=' + (Date.now() + data.expiresIn * 1000));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
writeFileSync(envPath, lines.join('\n') + '\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 登录
|
|
109
|
+
export async function login(username, password, env = 'uat') {
|
|
110
|
+
if (env) setEnvironment(env);
|
|
111
|
+
|
|
112
|
+
const baseUrl = getBaseUrl();
|
|
113
|
+
const t = Date.now();
|
|
114
|
+
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
|
|
115
|
+
|
|
116
|
+
let bodyParts = [
|
|
117
|
+
'--' + boundary,
|
|
118
|
+
'Content-Disposition: form-data; name="scope"',
|
|
119
|
+
'',
|
|
120
|
+
'read write',
|
|
121
|
+
'--' + boundary,
|
|
122
|
+
'Content-Disposition: form-data; name="grant_type"',
|
|
123
|
+
'',
|
|
124
|
+
'password',
|
|
125
|
+
'--' + boundary,
|
|
126
|
+
'Content-Disposition: form-data; name="username"',
|
|
127
|
+
'',
|
|
128
|
+
'+86' + username,
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
// UAT 环境加密密码
|
|
132
|
+
const keyData = await getPublicKey();
|
|
133
|
+
const publicKey = keyData.key;
|
|
134
|
+
const publicKeyId = keyData.id;
|
|
135
|
+
const encryptedPassword = await encryptPassword(password, publicKey);
|
|
136
|
+
|
|
137
|
+
bodyParts.push(
|
|
138
|
+
'--' + boundary,
|
|
139
|
+
'Content-Disposition: form-data; name="publicKeyId"',
|
|
140
|
+
'',
|
|
141
|
+
publicKeyId,
|
|
142
|
+
'--' + boundary,
|
|
143
|
+
'Content-Disposition: form-data; name="password"',
|
|
144
|
+
'',
|
|
145
|
+
encryptedPassword,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
bodyParts.push('--' + boundary + '--');
|
|
149
|
+
|
|
150
|
+
const response = await fetch(baseUrl + '/base/login?t=' + t, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
'accept': 'application/json',
|
|
154
|
+
'authorization': BASIC_AUTH,
|
|
155
|
+
'content-type': 'multipart/form-data; boundary=' + boundary,
|
|
156
|
+
},
|
|
157
|
+
body: bodyParts.join('\r\n'),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const data = await response.json();
|
|
161
|
+
|
|
162
|
+
if (data.error) {
|
|
163
|
+
const errMsg = data.message || data.error_description || JSON.stringify(data);
|
|
164
|
+
throw new Error('Login failed: ' + errMsg);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setToken(data.access_token);
|
|
168
|
+
|
|
169
|
+
// 保存凭证
|
|
170
|
+
saveCredentials({
|
|
171
|
+
username,
|
|
172
|
+
password,
|
|
173
|
+
accessToken: data.access_token,
|
|
174
|
+
refreshToken: data.refresh_token,
|
|
175
|
+
expiresIn: data.expires_in,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return data;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 使用保存的凭证登录
|
|
182
|
+
export async function loginWithEnv(forceReLogin = false) {
|
|
183
|
+
const username = process.env.BP_USER;
|
|
184
|
+
const password = process.env.BP_PASSWORD;
|
|
185
|
+
|
|
186
|
+
if (!username || !password) {
|
|
187
|
+
throw new Error('Please run "byteplan login" first');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 检查缓存的 token
|
|
191
|
+
if (!forceReLogin) {
|
|
192
|
+
const cachedToken = process.env.ACCESS_TOKEN;
|
|
193
|
+
const expiresAt = parseInt(process.env.TOKEN_EXPIRES_IN || '0');
|
|
194
|
+
|
|
195
|
+
if (cachedToken && expiresAt > Date.now() + 5 * 60 * 1000) {
|
|
196
|
+
setToken(cachedToken);
|
|
197
|
+
return { access_token: cachedToken, _cached: true };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return login(username, password, process.env.BP_ENV || 'uat');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 获取用户信息
|
|
205
|
+
export async function getUserInfo(token) {
|
|
206
|
+
const baseUrl = getBaseUrl();
|
|
207
|
+
const t = Date.now();
|
|
208
|
+
const response = await fetch(baseUrl + '/base/api/home?pageAuthFlag=true&t=' + t, {
|
|
209
|
+
method: 'GET',
|
|
210
|
+
headers: {
|
|
211
|
+
'accept': 'application/json, text/plain, */*',
|
|
212
|
+
'authorization': 'Bearer ' + (token || currentToken),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return response.json();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 切换租户
|
|
220
|
+
export async function switchTenant(token, tenantId) {
|
|
221
|
+
const baseUrl = getBaseUrl();
|
|
222
|
+
const t = Date.now();
|
|
223
|
+
const response = await fetch(baseUrl + '/base/api/user/tenant/switch?enabled=1&tenantId=' + tenantId + '&t=' + t, {
|
|
224
|
+
method: 'PUT',
|
|
225
|
+
headers: {
|
|
226
|
+
'accept': 'application/json, text/plain, */*',
|
|
227
|
+
'authorization': 'Bearer ' + (token || currentToken),
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return response.json();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 查询模型列表
|
|
235
|
+
export async function queryModels(token, options = {}) {
|
|
236
|
+
const { page = 0, size = 100 } = options;
|
|
237
|
+
const baseUrl = getBaseUrl();
|
|
238
|
+
|
|
239
|
+
const headers = {
|
|
240
|
+
'accept': 'application/json, text/plain, */*',
|
|
241
|
+
'authorization': 'Bearer ' + (token || currentToken),
|
|
242
|
+
...DEFAULT_MENU_HEADERS,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const response = await fetch(baseUrl + '/data/api/model/query?page=' + page + '&size=' + size, {
|
|
246
|
+
method: 'GET',
|
|
247
|
+
headers,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return response.json();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 获取模型字段
|
|
254
|
+
export async function getModelColumns(token, modelCodes) {
|
|
255
|
+
const baseUrl = getBaseUrl();
|
|
256
|
+
const response = await fetch(baseUrl + '/data/api/model/col/get/model/col/by/codes', {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: {
|
|
259
|
+
'accept': 'application/json, text/plain, */*',
|
|
260
|
+
'authorization': 'Bearer ' + (token || currentToken),
|
|
261
|
+
'content-type': 'application/json',
|
|
262
|
+
...DEFAULT_MENU_HEADERS,
|
|
263
|
+
},
|
|
264
|
+
body: JSON.stringify(modelCodes),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return response.json();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 查询模型数据
|
|
271
|
+
export async function getModelData(token, modelCode, params = {}) {
|
|
272
|
+
const baseUrl = getBaseUrl();
|
|
273
|
+
const response = await fetch(baseUrl + '/foresight/api/bi/multdim/anls/ai/query', {
|
|
274
|
+
method: 'POST',
|
|
275
|
+
headers: {
|
|
276
|
+
'accept': 'application/json, text/plain, */*',
|
|
277
|
+
'authorization': 'Bearer ' + (token || currentToken),
|
|
278
|
+
'content-type': 'application/json',
|
|
279
|
+
...DEFAULT_MENU_HEADERS,
|
|
280
|
+
},
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
modelCode,
|
|
283
|
+
...params,
|
|
284
|
+
}),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return response.json();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 获取维度值
|
|
291
|
+
export async function getDimValues(token, dimCode, options = {}) {
|
|
292
|
+
const { page = 0, size = 100 } = options;
|
|
293
|
+
const baseUrl = getBaseUrl();
|
|
294
|
+
|
|
295
|
+
const response = await fetch(baseUrl + '/data/api/online/lov/DATA_DIM_VALUE_LOV?dimCode=' + dimCode + '&page=' + page + '&size=' + size, {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
headers: {
|
|
298
|
+
'accept': 'application/json, text/plain, */*',
|
|
299
|
+
'authorization': 'Bearer ' + (token || currentToken),
|
|
300
|
+
'content-type': 'application/json',
|
|
301
|
+
...DEFAULT_MENU_HEADERS,
|
|
302
|
+
},
|
|
303
|
+
body: JSON.stringify({ dimCode, page, size }),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return response.json();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 获取 LOV 值
|
|
310
|
+
export async function getLovValues(token, lovCode, options = {}) {
|
|
311
|
+
const { keywords = null } = options;
|
|
312
|
+
const baseUrl = getBaseUrl();
|
|
313
|
+
|
|
314
|
+
const body = {};
|
|
315
|
+
if (keywords) body.keywords = keywords;
|
|
316
|
+
|
|
317
|
+
const response = await fetch(baseUrl + '/data/api/online/lov/' + lovCode, {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
headers: {
|
|
320
|
+
'accept': 'application/json, text/plain, */*',
|
|
321
|
+
'authorization': 'Bearer ' + (token || currentToken),
|
|
322
|
+
'content-type': 'application/json',
|
|
323
|
+
...DEFAULT_MENU_HEADERS,
|
|
324
|
+
},
|
|
325
|
+
body: JSON.stringify(body),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return response.json();
|
|
329
|
+
}
|