neo-cmp-cli 1.7.3 → 1.7.5-beta.1
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 +355 -201
- package/package.json +7 -5
- package/src/config/auth.config.js +23 -0
- package/src/config/default.config.js +25 -0
- package/src/initData/neo.config.js +20 -11
- package/src/module/index.js +122 -80
- package/src/neo/neoLogin.js +451 -0
- package/src/neo/neoService.js +86 -8
- package/src/template/antd-custom-cmp-template/neo.config.js +19 -10
- package/src/template/antd-custom-cmp-template/package.json +1 -1
- package/src/template/echarts-custom-cmp-template/neo.config.js +20 -11
- package/src/template/echarts-custom-cmp-template/package.json +1 -1
- package/src/template/empty-custom-cmp-template/neo.config.js +19 -10
- package/src/template/empty-custom-cmp-template/package.json +1 -1
- package/src/template/neo-custom-cmp-template/neo.config.js +15 -6
- package/src/template/neo-custom-cmp-template/package.json +1 -1
- package/src/template/react-custom-cmp-template/neo.config.js +19 -10
- package/src/template/react-custom-cmp-template/package.json +1 -1
- package/src/template/react-ts-custom-cmp-template/neo.config.js +19 -10
- package/src/template/react-ts-custom-cmp-template/package.json +1 -1
- package/src/template/vue2-custom-cmp-template/neo.config.js +19 -10
- package/src/template/vue2-custom-cmp-template/package.json +1 -1
- package/src/utils/cmpUtils/createCmpByZip.js +6 -0
- package/test/deprecate-versions.js +1 -1
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const ora = require('ora');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const url = require('url');
|
|
7
|
+
const open = require('open');
|
|
8
|
+
const portfinder = require('portfinder');
|
|
9
|
+
const { errorLog, successLog } = require('../utils/common');
|
|
10
|
+
const neoAuthConfig = require('../config/auth.config');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Neo 登录授权服务类
|
|
14
|
+
* 实现 OAuth2 授权码模式的登录/登出功能
|
|
15
|
+
*/
|
|
16
|
+
class NeoLoginService {
|
|
17
|
+
/**
|
|
18
|
+
* 初始化登录服务
|
|
19
|
+
* @param {object} config 授权配置信息
|
|
20
|
+
* @param {string} config.loginURL 登录授权 URL
|
|
21
|
+
* @param {string} config.tokenAPI Token 获取接口地址
|
|
22
|
+
* @param {string} config.response_type 认证类型 (code)
|
|
23
|
+
* @param {string} config.client_id 客户端 ID
|
|
24
|
+
* @param {string} config.client_secret 客户端秘钥
|
|
25
|
+
* @param {string} config.scope 授权范围 (all)
|
|
26
|
+
* @param {string} config.oauthType OAuth 类型 (standard)
|
|
27
|
+
* @param {string} config.access_type 访问类型 (offline/online)
|
|
28
|
+
* @param {string} config.grant_type 授权类型 (authorization_code)
|
|
29
|
+
*/
|
|
30
|
+
constructor(config = {}) {
|
|
31
|
+
const { loginURL, tokenAPI } = config;
|
|
32
|
+
|
|
33
|
+
// 验证必需的配置项
|
|
34
|
+
if (!loginURL || !tokenAPI) {
|
|
35
|
+
throw new Error('auth.config.js 配置不完整,需要包含 loginURL、tokenAPI');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.loginURL = loginURL;
|
|
39
|
+
this.tokenAPI = tokenAPI;
|
|
40
|
+
this.response_type = neoAuthConfig.response_type || 'code';
|
|
41
|
+
this.client_id = neoAuthConfig.client_id;
|
|
42
|
+
this.client_secret = neoAuthConfig.client_secret;
|
|
43
|
+
this.scope = neoAuthConfig.scope || 'all';
|
|
44
|
+
this.oauthType = neoAuthConfig.oauthType || 'standard';
|
|
45
|
+
this.access_type = neoAuthConfig.access_type || 'offline';
|
|
46
|
+
this.grant_type = neoAuthConfig.grant_type || 'authorization_code';
|
|
47
|
+
|
|
48
|
+
// Token 存储路径(在当前项目根目录的 .neo-cli 文件夹下)
|
|
49
|
+
this.tokenDir = path.join(process.cwd(), '.neo-cli');
|
|
50
|
+
this.tokenFile = path.join(this.tokenDir, 'token.json');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 确保 token 存储目录存在
|
|
55
|
+
*/
|
|
56
|
+
ensureTokenDir() {
|
|
57
|
+
if (!fs.existsSync(this.tokenDir)) {
|
|
58
|
+
fs.mkdirSync(this.tokenDir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 保存 token 到本地文件
|
|
64
|
+
* @param {object} tokenData Token 数据
|
|
65
|
+
*/
|
|
66
|
+
saveToken(tokenData) {
|
|
67
|
+
this.ensureTokenDir();
|
|
68
|
+
const tokenInfo = {
|
|
69
|
+
...tokenData,
|
|
70
|
+
savedAt: Date.now(),
|
|
71
|
+
expiresAt: Date.now() + (tokenData.expires_in || 7200) * 1000
|
|
72
|
+
};
|
|
73
|
+
fs.writeFileSync(this.tokenFile, JSON.stringify(tokenInfo, null, 2), 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 读取本地保存的 token
|
|
78
|
+
* @returns {object|null} Token 数据,如果不存在或已过期则返回 null
|
|
79
|
+
*/
|
|
80
|
+
readToken() {
|
|
81
|
+
if (!fs.existsSync(this.tokenFile)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const tokenData = JSON.parse(fs.readFileSync(this.tokenFile, 'utf-8'));
|
|
87
|
+
return tokenData;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
errorLog(`读取 token 文件失败: ${error.message}`);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 检查 token 是否过期
|
|
96
|
+
* @param {object} tokenData Token 数据
|
|
97
|
+
* @returns {boolean} true 表示已过期,false 表示未过期
|
|
98
|
+
*/
|
|
99
|
+
isTokenExpired(tokenData) {
|
|
100
|
+
if (!tokenData || !tokenData.expiresAt) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
// 提前 5 分钟判断为过期,避免边缘情况
|
|
104
|
+
return Date.now() >= tokenData.expiresAt - 5 * 60 * 1000;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 清除本地保存的 token
|
|
109
|
+
*/
|
|
110
|
+
clearToken() {
|
|
111
|
+
if (fs.existsSync(this.tokenFile)) {
|
|
112
|
+
fs.unlinkSync(this.tokenFile);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 生成回调地址
|
|
118
|
+
* @param {number} port 端口号
|
|
119
|
+
* @returns {string} 回调地址
|
|
120
|
+
*/
|
|
121
|
+
getRedirectURI(port) {
|
|
122
|
+
return `http://localhost:${port}/callback`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 构建授权 URL
|
|
127
|
+
* @param {string} redirectUri 回调地址
|
|
128
|
+
* @returns {string} 授权 URL
|
|
129
|
+
*/
|
|
130
|
+
buildAuthUrl(redirectUri) {
|
|
131
|
+
const params = new URLSearchParams({
|
|
132
|
+
response_type: this.response_type,
|
|
133
|
+
client_id: this.client_id,
|
|
134
|
+
redirect_uri: redirectUri,
|
|
135
|
+
scope: this.scope,
|
|
136
|
+
oauthType: this.oauthType,
|
|
137
|
+
access_type: this.access_type
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return `${this.loginURL}?${params.toString()}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 打开浏览器访问授权 URL
|
|
145
|
+
* @param {string} authUrl 授权 URL
|
|
146
|
+
*/
|
|
147
|
+
async openBrowser(authUrl) {
|
|
148
|
+
try {
|
|
149
|
+
await open(authUrl);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
errorLog(`无法自动打开浏览器: ${error.message}`);
|
|
152
|
+
console.log(`\n请手动访问以下 URL 进行授权:\n${authUrl}\n`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 启动本地服务器接收授权码(code)
|
|
158
|
+
* @returns {Promise<{redirectUri: string, codePromise: Promise<string>}>} 回调地址和授权码Promise
|
|
159
|
+
*/
|
|
160
|
+
async startCallbackServer() {
|
|
161
|
+
// 使用 portfinder 获取可用端口
|
|
162
|
+
const port = await portfinder.getPortPromise({
|
|
163
|
+
port: 8888, // 起始端口
|
|
164
|
+
stopPort: 9999 // 结束端口
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// 使用获取到的端口生成 redirect_uri
|
|
168
|
+
const redirectUri = this.getRedirectURI(port);
|
|
169
|
+
const redirectUrl = new URL(redirectUri);
|
|
170
|
+
|
|
171
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
172
|
+
const server = http.createServer((req, res) => {
|
|
173
|
+
const parsedUrl = url.parse(req.url, true);
|
|
174
|
+
|
|
175
|
+
if (parsedUrl.pathname === redirectUrl.pathname || parsedUrl.pathname === '/') {
|
|
176
|
+
const code = parsedUrl.query.code;
|
|
177
|
+
const error = parsedUrl.query.error;
|
|
178
|
+
|
|
179
|
+
if (error) {
|
|
180
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
181
|
+
res.end(`
|
|
182
|
+
<html>
|
|
183
|
+
<head><title>授权失败</title></head>
|
|
184
|
+
<body>
|
|
185
|
+
<h1>授权失败</h1>
|
|
186
|
+
<p>错误信息: ${error}</p>
|
|
187
|
+
<p>您可以关闭此窗口</p>
|
|
188
|
+
</body>
|
|
189
|
+
</html>
|
|
190
|
+
`);
|
|
191
|
+
server.close();
|
|
192
|
+
reject(new Error(`授权失败: ${error}`));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (code) {
|
|
197
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
198
|
+
res.end(`
|
|
199
|
+
<html>
|
|
200
|
+
<head><title>授权成功</title></head>
|
|
201
|
+
<body>
|
|
202
|
+
<h1>授权成功!</h1>
|
|
203
|
+
<p>已获取授权码,正在获取 token...</p>
|
|
204
|
+
<p>您可以关闭此窗口</p>
|
|
205
|
+
<script>window.close();</script>
|
|
206
|
+
</body>
|
|
207
|
+
</html>
|
|
208
|
+
`);
|
|
209
|
+
server.close();
|
|
210
|
+
resolve(code);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
215
|
+
res.end(`
|
|
216
|
+
<html>
|
|
217
|
+
<head><title>授权失败</title></head>
|
|
218
|
+
<body>
|
|
219
|
+
<h1>授权失败</h1>
|
|
220
|
+
<p>未获取到授权码</p>
|
|
221
|
+
<p>您可以关闭此窗口</p>
|
|
222
|
+
</body>
|
|
223
|
+
</html>
|
|
224
|
+
`);
|
|
225
|
+
server.close();
|
|
226
|
+
reject(new Error('未获取到授权码'));
|
|
227
|
+
} else {
|
|
228
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
229
|
+
res.end('Not Found');
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
server.on('error', (error) => {
|
|
234
|
+
if (error.code === 'EADDRINUSE') {
|
|
235
|
+
reject(new Error(`端口 ${port} 已被占用,无法启动回调服务器`));
|
|
236
|
+
} else {
|
|
237
|
+
reject(error);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
server.listen(port, () => {
|
|
242
|
+
console.log(`\n本地回调服务器已启动,监听端口: ${port}`);
|
|
243
|
+
console.log(`回调地址: ${redirectUri}`);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// 设置超时(5 分钟)
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
server.close();
|
|
249
|
+
reject(new Error('授权超时,请重试'));
|
|
250
|
+
}, 5 * 60 * 1000);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return { redirectUri, codePromise };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 使用授权码获取 token
|
|
258
|
+
* @param {string} code 授权码
|
|
259
|
+
* @param {string} redirectUri 回调地址
|
|
260
|
+
* @returns {Promise<object>} Token 数据
|
|
261
|
+
*/
|
|
262
|
+
async getTokenByCode(code, redirectUri) {
|
|
263
|
+
const spinner = ora('正在获取 access token...').start();
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const formData = new URLSearchParams();
|
|
267
|
+
formData.append('grant_type', this.grant_type);
|
|
268
|
+
formData.append('client_id', this.client_id);
|
|
269
|
+
formData.append('client_secret', this.client_secret);
|
|
270
|
+
formData.append('code', code);
|
|
271
|
+
formData.append('redirect_uri', redirectUri);
|
|
272
|
+
|
|
273
|
+
const response = await axios.post(this.tokenAPI, formData.toString(), {
|
|
274
|
+
headers: {
|
|
275
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const tokenData = response.data;
|
|
280
|
+
|
|
281
|
+
if (!tokenData || !tokenData.access_token) {
|
|
282
|
+
errorLog('获取 token 失败:响应中未包含 access_token', spinner);
|
|
283
|
+
errorLog(`响应数据: ${JSON.stringify(tokenData)}`);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
successLog('成功获取 access token', spinner);
|
|
288
|
+
return tokenData;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
errorLog('获取 token 失败', spinner);
|
|
291
|
+
errorLog(`\n获取 token 失败: ${error.message}`);
|
|
292
|
+
if (error.response) {
|
|
293
|
+
errorLog(`响应数据: ${JSON.stringify(error.response.data)}`);
|
|
294
|
+
}
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 使用 refresh_token 刷新 token
|
|
301
|
+
* @param {string} refreshToken Refresh Token
|
|
302
|
+
* @returns {Promise<object>} 新的 Token 数据
|
|
303
|
+
*/
|
|
304
|
+
async refreshToken(refreshToken) {
|
|
305
|
+
const spinner = ora('正在刷新 token...').start();
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const formData = new URLSearchParams();
|
|
309
|
+
formData.append('grant_type', 'refresh_token');
|
|
310
|
+
formData.append('client_id', this.client_id);
|
|
311
|
+
formData.append('client_secret', this.client_secret);
|
|
312
|
+
formData.append('refresh_token', refreshToken);
|
|
313
|
+
|
|
314
|
+
const response = await axios.post(this.tokenAPI, formData.toString(), {
|
|
315
|
+
headers: {
|
|
316
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const tokenData = response.data;
|
|
321
|
+
|
|
322
|
+
if (!tokenData || !tokenData.access_token) {
|
|
323
|
+
errorLog('刷新 token 失败:响应中未包含 access_token', spinner);
|
|
324
|
+
errorLog(`响应数据: ${JSON.stringify(tokenData)}`);
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
successLog('Token 刷新成功', spinner);
|
|
329
|
+
return tokenData;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
errorLog('刷新 token 失败', spinner);
|
|
332
|
+
errorLog(`\n刷新 token 失败: ${error.message}`);
|
|
333
|
+
if (error.response) {
|
|
334
|
+
errorLog(`响应数据: ${JSON.stringify(error.response.data)}`);
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 执行登录流程
|
|
342
|
+
*/
|
|
343
|
+
async login() {
|
|
344
|
+
console.log('\n========== NeoCRM 登录授权 ==========\n');
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
// 1. 启动本地回调服务器(自动获取可用端口)
|
|
348
|
+
const { redirectUri, codePromise } = await this.startCallbackServer();
|
|
349
|
+
|
|
350
|
+
// 2. 构建授权 URL
|
|
351
|
+
const authUrl = this.buildAuthUrl(redirectUri);
|
|
352
|
+
console.log('授权 URL:', authUrl);
|
|
353
|
+
|
|
354
|
+
// 3. 打开浏览器进行授权
|
|
355
|
+
console.log('\n正在打开浏览器进行授权...');
|
|
356
|
+
await this.openBrowser(authUrl);
|
|
357
|
+
|
|
358
|
+
// 4. 等待获取授权码
|
|
359
|
+
const code = await codePromise;
|
|
360
|
+
console.log('\n✓ 已获取授权码');
|
|
361
|
+
|
|
362
|
+
// 5. 使用授权码获取 token
|
|
363
|
+
const tokenData = await this.getTokenByCode(code, redirectUri);
|
|
364
|
+
|
|
365
|
+
// 6. 保存 token 到本地
|
|
366
|
+
this.saveToken(tokenData);
|
|
367
|
+
|
|
368
|
+
console.log('\n========== 登录成功 ==========\n');
|
|
369
|
+
console.log(`Token 已保存到: ${this.tokenFile}`);
|
|
370
|
+
console.log(`实例地址: ${tokenData.instance_uri || '未返回'}`);
|
|
371
|
+
console.log(`租户 ID: ${tokenData.tenant_id || '未返回'}`);
|
|
372
|
+
console.log(`Token 有效期: ${tokenData.expires_in || 7200} 秒`);
|
|
373
|
+
console.log(`Refresh Token 有效期: ${tokenData.refresh_token_expires_in || 2592000} 秒`);
|
|
374
|
+
console.log('');
|
|
375
|
+
|
|
376
|
+
return tokenData;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
errorLog(`\n登录失败: ${error.message}`);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* 执行登出流程
|
|
385
|
+
*/
|
|
386
|
+
async logout() {
|
|
387
|
+
console.log('\n========== NeoCRM 登出 ==========\n');
|
|
388
|
+
|
|
389
|
+
if (!fs.existsSync(this.tokenFile)) {
|
|
390
|
+
console.log('当前未登录,无需登出。');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
this.clearToken();
|
|
396
|
+
successLog(`已清除 token 文件: ${this.tokenFile}`);
|
|
397
|
+
console.log('\n登出成功!\n');
|
|
398
|
+
} catch (error) {
|
|
399
|
+
errorLog(`登出失败: ${error.message}`);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 获取有效的 token(自动刷新)
|
|
406
|
+
* @returns {Promise<object>} Token 数据
|
|
407
|
+
*/
|
|
408
|
+
async getValidToken() {
|
|
409
|
+
const tokenData = this.readToken();
|
|
410
|
+
|
|
411
|
+
if (!tokenData) {
|
|
412
|
+
errorLog('未找到本地 token,请先执行 neo login 进行登录');
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 检查 token 是否过期
|
|
417
|
+
if (this.isTokenExpired(tokenData)) {
|
|
418
|
+
console.log('Token 已过期,正在尝试刷新...');
|
|
419
|
+
|
|
420
|
+
if (!tokenData.refresh_token) {
|
|
421
|
+
errorLog('Refresh token 不存在,请重新登录');
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 使用 refresh_token 刷新
|
|
426
|
+
const newTokenData = await this.refreshToken(tokenData.refresh_token);
|
|
427
|
+
|
|
428
|
+
if (!newTokenData) {
|
|
429
|
+
errorLog('Token 刷新失败,请重新登录 (neo login)');
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 保存新的 token
|
|
434
|
+
this.saveToken(newTokenData);
|
|
435
|
+
return newTokenData;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return tokenData;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* 获取 access token 字符串
|
|
443
|
+
* @returns {Promise<string>} Access Token
|
|
444
|
+
*/
|
|
445
|
+
async getAccessToken() {
|
|
446
|
+
const tokenData = await this.getValidToken();
|
|
447
|
+
return tokenData.access_token;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
module.exports = NeoLoginService;
|
package/src/neo/neoService.js
CHANGED
|
@@ -5,12 +5,14 @@ const path = require('path');
|
|
|
5
5
|
const ora = require('ora');
|
|
6
6
|
const _ = require('lodash');
|
|
7
7
|
const { resolve } = require('akfun');
|
|
8
|
+
const NeoLoginService = require('./neoLogin');
|
|
8
9
|
const updatePublishLog = require('../utils/projectUtils/updatePublishLog');
|
|
9
10
|
const { getFramework, errorLog, successLog } = require('../utils/common');
|
|
10
11
|
|
|
11
12
|
// NeoCRM 平台默认 API 配置
|
|
12
13
|
const NeoCrmAPI = {
|
|
13
14
|
neoBaseURL: 'https://crm.xiaoshouyi.com', // 平台根地址
|
|
15
|
+
loginAPI: 'https://login-cd.xiaoshouyi.com/auc/oauth2/auth', // code 获取接口地址
|
|
14
16
|
tokenAPI: 'https://login.xiaoshouyi.com/auc/oauth2/token', // Token 获取接口地址
|
|
15
17
|
uploadAPI: '/rest/metadata/v3.0/ui/customComponents/actions/upload', // 文件上传接口地址
|
|
16
18
|
delete: '/rest/metadata/v3.0/ui/customComponents',
|
|
@@ -48,28 +50,47 @@ class NeoService {
|
|
|
48
50
|
/**
|
|
49
51
|
* 初始化 Neo 服务
|
|
50
52
|
* @param {object} config 配置信息
|
|
53
|
+
* @param {string} config.authType 授权类型,可选值:oauth2(默认)、password
|
|
51
54
|
* @param {string} config.neoBaseURL Neo 平台根地址
|
|
52
55
|
* @param {string} config.tokenAPI Token 获取接口地址
|
|
56
|
+
* @param {string} config.loginURL OAuth2 登录授权 URL(OAuth2 模式需要)
|
|
53
57
|
* @param {object} config.auth 授权信息
|
|
54
58
|
* @param {string} config.auth.client_id 客户端 ID
|
|
55
59
|
* @param {string} config.auth.client_secret 客户端密钥
|
|
56
|
-
* @param {string} config.auth.username
|
|
57
|
-
* @param {string} config.auth.password
|
|
60
|
+
* @param {string} config.auth.username 用户名(password 模式需要)
|
|
61
|
+
* @param {string} config.auth.password 密码(password 模式需要)
|
|
58
62
|
*/
|
|
59
63
|
constructor(config = {}) {
|
|
60
|
-
const { assetsRoot, neoBaseURL, tokenAPI, auth } = config || {};
|
|
64
|
+
const { assetsRoot, neoBaseURL, tokenAPI, loginURL, auth, authType } = config || {};
|
|
65
|
+
|
|
66
|
+
// 设置授权类型,默认为 oauth2
|
|
67
|
+
this.authType = authType || 'oauth2';
|
|
68
|
+
|
|
61
69
|
if (!auth) {
|
|
62
70
|
throw new Error('auth 不能为空');
|
|
63
71
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
)
|
|
72
|
+
|
|
73
|
+
// 根据授权类型验证必需的配置项
|
|
74
|
+
if (this.authType === 'password') {
|
|
75
|
+
if (!auth.client_id || !auth.client_secret || !auth.username || !auth.password) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'neo.config.js / neoConfig / auth 配置不完整(password 模式),需要包含 client_id、client_secret、username、password'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
} else if (this.authType === 'oauth2') {
|
|
81
|
+
if (!loginURL || !tokenAPI) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'neo.config.js / neoConfig 配置不完整(oauth2 模式),需要包含 loginURL、tokenAPI 配置。'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
throw new Error(`不支持的授权类型: ${this.authType},可选值:oauth2、password`);
|
|
68
88
|
}
|
|
69
89
|
|
|
70
90
|
this.assetsRoot = assetsRoot || resolve('dist');
|
|
71
91
|
this.neoBaseURL = neoBaseURL || NeoCrmAPI.neoBaseURL;
|
|
72
92
|
this.tokenAPI = tokenAPI || NeoCrmAPI.tokenAPI;
|
|
93
|
+
this.loginURL = loginURL || NeoCrmAPI.loginAPI;
|
|
73
94
|
this.auth = auth;
|
|
74
95
|
this.cmpList = [];
|
|
75
96
|
this.cmpInfoMap = {};
|
|
@@ -117,10 +138,30 @@ class NeoService {
|
|
|
117
138
|
|
|
118
139
|
/**
|
|
119
140
|
* 获取 token(含授权信息、租户信息)
|
|
141
|
+
* 根据 authType 自动选择授权模式:oauth2 或 password
|
|
120
142
|
* @returns {Promise<string>} token
|
|
121
143
|
*/
|
|
122
144
|
async getToken() {
|
|
123
|
-
|
|
145
|
+
// 检查缓存是否有效
|
|
146
|
+
if (!this.isTokenExpired()) {
|
|
147
|
+
return this.tokenCache.token;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 根据授权类型选择对应的获取方式
|
|
151
|
+
if (this.authType === 'oauth2') {
|
|
152
|
+
return await this.getTokenByOAuth2();
|
|
153
|
+
} else {
|
|
154
|
+
return await this.getTokenByPassword();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 使用密码模式获取 token
|
|
160
|
+
* @returns {Promise<string>} token
|
|
161
|
+
*/
|
|
162
|
+
async getTokenByPassword() {
|
|
163
|
+
const spinner = ora('获取 token(密码模式)...').start();
|
|
164
|
+
|
|
124
165
|
// 检查缓存是否有效
|
|
125
166
|
if (!this.isTokenExpired()) {
|
|
126
167
|
successLog('使用缓存的 token。', spinner);
|
|
@@ -176,6 +217,43 @@ class NeoService {
|
|
|
176
217
|
}
|
|
177
218
|
}
|
|
178
219
|
|
|
220
|
+
/**
|
|
221
|
+
* 获取 OAuth2 授权码模式的 token
|
|
222
|
+
* @returns {Promise<string>} token
|
|
223
|
+
*/
|
|
224
|
+
async getTokenByOAuth2() {
|
|
225
|
+
const spinner = ora('获取 token(OAuth2 模式)...').start();
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
// 构建 OAuth2 配置
|
|
229
|
+
const oauth2Config = {
|
|
230
|
+
loginURL: this.loginURL,
|
|
231
|
+
tokenAPI: this.tokenAPI
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const loginService = new NeoLoginService(oauth2Config);
|
|
235
|
+
const accessToken = await loginService.getAccessToken();
|
|
236
|
+
|
|
237
|
+
// 缓存 token(OAuth2 模式下的 token 有效期由 NeoLoginService 管理)
|
|
238
|
+
// 这里我们设置一个较长的过期时间,实际过期检查由 NeoLoginService 内部处理
|
|
239
|
+
this.tokenCache = {
|
|
240
|
+
token: accessToken,
|
|
241
|
+
expiresAt: Date.now() + 7200 * 1000 // 默认 2 小时,实际由 NeoLoginService 管理
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
spinner.clear();
|
|
245
|
+
spinner.stop();
|
|
246
|
+
return accessToken;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
errorLog('获取 token 失败(OAuth2 模式)', spinner);
|
|
249
|
+
errorLog(`\n获取 token 失败: ${error.message}`);
|
|
250
|
+
if (error.response) {
|
|
251
|
+
errorLog(`响应数据: ${JSON.stringify(error.response.data)}`);
|
|
252
|
+
}
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
179
257
|
/**
|
|
180
258
|
* 刷新 token
|
|
181
259
|
* @returns {Promise<string>} 新的 token
|
|
@@ -103,22 +103,31 @@ module.exports = {
|
|
|
103
103
|
}
|
|
104
104
|
*/
|
|
105
105
|
},
|
|
106
|
-
// NeoCRM 平台配置
|
|
106
|
+
// 授权码授权模式下的 NeoCRM 平台配置
|
|
107
107
|
neoConfig: {
|
|
108
|
+
authType: 'oauth2', // 授权类型,可选值:oauth2(默认)、password
|
|
108
109
|
neoBaseURL: 'https://crm-cd.xiaoshouyi.com', // 平台根地址(默认:https://crm.xiaoshouyi.com)
|
|
110
|
+
// 当 authType 为 oauth2 时,loginURL 配置项必填
|
|
111
|
+
loginURL: 'https://login-cd.xiaoshouyi.com/auc/oauth2/auth', // 登录授权 URL(默认:https://login.xiaoshouyi.com/auc/oauth2/auth)
|
|
109
112
|
tokenAPI: 'https://login-cd.xiaoshouyi.com/auc/oauth2/token', // Token 获取接口地址(默认:https://login.xiaoshouyi.com/auc/oauth2/token)
|
|
110
|
-
|
|
113
|
+
},
|
|
114
|
+
/*
|
|
115
|
+
// 密码授权模式下的 NeoCRM 平台配置
|
|
116
|
+
neoConfig: {
|
|
117
|
+
authType: 'password', // 授权类型,可选值:oauth2(默认)、password
|
|
118
|
+
neoBaseURL: 'https://crm-cd.xiaoshouyi.com', // 平台根地址(默认:https://crm.xiaoshouyi.com)
|
|
119
|
+
tokenAPI: 'https://login-cd.xiaoshouyi.com/auc/oauth2/token', // Token 获取接口地址(默认:https://login.xiaoshouyi.com/auc/oauth2/token)
|
|
120
|
+
// 当 authType 为 password 时,auth 配置项必填
|
|
111
121
|
auth: {
|
|
112
|
-
client_id: 'xx', // 客户端 ID,从创建连接器的客户端信息中获取(Client_Id)
|
|
113
|
-
client_secret: 'xxx', // 客户端秘钥,从创建连接器的客户端信息中获取(Client_Secret)
|
|
114
|
-
username: 'xx', // 用户在销售易系统中的用户名
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
*/
|
|
119
|
-
password: 'xx xx', // 用户账户密码 + 8 位安全令牌
|
|
122
|
+
client_id: auth.client_id || 'xx', // 客户端 ID,从创建连接器的客户端信息中获取(Client_Id)
|
|
123
|
+
client_secret: auth.client_secret || 'xxx', // 客户端秘钥,从创建连接器的客户端信息中获取(Client_Secret)
|
|
124
|
+
username: auth.username || 'xx', // 用户在销售易系统中的用户名
|
|
125
|
+
// password 为 用户在销售易系统中的账号密码加上 8 位安全令牌。
|
|
126
|
+
// 例如,用户密码为 123456,安全令牌为 ABCDEFGH,则 password 的值应为 123456ABCDEFGH。
|
|
127
|
+
password: auth.password || 'xx xx', // 用户账户密码 + 8 位安全令牌
|
|
120
128
|
},
|
|
121
129
|
},
|
|
130
|
+
*/
|
|
122
131
|
pushCmp: {
|
|
123
132
|
// 用于构建并发布至 NeoCRM 的相关配置
|
|
124
133
|
/*
|