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 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
+ }