@plugin-light/shared 1.0.3 → 1.0.14
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/lib/cdn/index.d.ts +1 -0
- package/lib/css/index.d.ts +1 -1
- package/lib/email/index.d.ts +1 -0
- package/lib/email/watch-unread.d.ts +103 -0
- package/lib/font-project/config.d.ts +15 -0
- package/lib/font-project/font-project.d.ts +75 -0
- package/lib/font-project/index.d.ts +2 -0
- package/lib/icon-project/config.d.ts +23 -0
- package/lib/icon-project/icon-project.d.ts +83 -0
- package/lib/icon-project/index.d.ts +2 -0
- package/lib/image/config.d.ts +23 -0
- package/lib/image/image.d.ts +67 -0
- package/lib/image/index.d.ts +3 -0
- package/lib/image/upload-core.d.ts +83 -0
- package/lib/index.d.ts +34 -20
- package/lib/index.js +2069 -342
- package/lib/index.mjs +2506 -0
- package/lib/landun/cos-sync-to-git.d.ts +12 -0
- package/lib/landun/index.d.ts +2 -0
- package/lib/landun/start.d.ts +42 -0
- package/lib/mcp/auth.d.ts +24 -0
- package/lib/mcp/http.d.ts +34 -0
- package/lib/mcp/index.d.ts +3 -0
- package/lib/mcp/mcp.d.ts +27 -0
- package/lib/mp-ci/config.d.ts +11 -0
- package/lib/mp-ci/helper.d.ts +12 -0
- package/lib/mp-ci/index.d.ts +5 -0
- package/lib/mp-ci/mp-ci.d.ts +73 -0
- package/lib/mp-ci/record.d.ts +39 -0
- package/lib/mp-ci/start-from-mcp.d.ts +42 -0
- package/lib/npm-publish/index.d.ts +1 -0
- package/lib/pipeline/index.d.ts +1 -0
- package/lib/pipeline/pipeline.d.ts +4 -0
- package/lib/pipeline-npm-publish/core.d.ts +53 -0
- package/lib/pipeline-npm-publish/helper.d.ts +7 -0
- package/lib/pipeline-npm-publish/index.d.ts +2 -0
- package/lib/repo/index.d.ts +1 -0
- package/lib/repo/repo.d.ts +7 -0
- package/lib/tinypng/index.d.ts +1 -0
- package/lib/tinypng/tinypng.d.ts +18 -0
- package/lib/white-user/config.d.ts +53 -0
- package/lib/white-user/cron.d.ts +15 -0
- package/lib/white-user/index.d.ts +3 -0
- package/lib/white-user/update-cos.d.ts +47 -0
- package/package.json +25 -3
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,2506 @@
|
|
|
1
|
+
import { getCdnList, NPM_PACKAGES_LIST } from '@plugin-light/const';
|
|
2
|
+
export { NPM_PACKAGES_LIST, getCdnList } from '@plugin-light/const';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { readEnvVariable, readFileSync, purgeUrlCache, uploadCOSStreamFile, getGitAuthor, getGitCurBranch, timeStampFormat, getGitCommitInfo, isWindows, normalizePath as normalizePath$1, fetchRainbowConfigFromSdk, queryGroupInfo, updateRainbowKVAndPublish, getAllDevopsTemplateInstances, safeJsonParse, uploadCOSFile, batchSendWxRobotMarkdown, writeFileSync } from 't-comm';
|
|
6
|
+
import path$1 from 'node:path';
|
|
7
|
+
import { ImapFlow } from 'imapflow';
|
|
8
|
+
import { simpleParser } from 'mailparser';
|
|
9
|
+
import TurndownService from 'turndown';
|
|
10
|
+
import axios from 'axios';
|
|
11
|
+
import { getOptions } from 'loader-utils';
|
|
12
|
+
import fs$1 from 'node:fs';
|
|
13
|
+
import tinify from 'tinify';
|
|
14
|
+
import { Cron } from 'croner';
|
|
15
|
+
|
|
16
|
+
function isInBlackList(filePath, blackList) {
|
|
17
|
+
if (!blackList) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const found = blackList.find((item) => {
|
|
21
|
+
if (typeof item === 'string') {
|
|
22
|
+
return filePath.includes(item);
|
|
23
|
+
}
|
|
24
|
+
return item.test(filePath);
|
|
25
|
+
});
|
|
26
|
+
return !!found;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function checkBundleAnalyze() {
|
|
30
|
+
return process.argv.includes('--bundleAnalyzer')
|
|
31
|
+
|| !!process.env.npm_config_report;
|
|
32
|
+
}
|
|
33
|
+
function checkDebugMode() {
|
|
34
|
+
return !!process.env.DEBUG_MODE;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const PLATFORM_MAP = {
|
|
38
|
+
MP_WX: 'mp-weixin',
|
|
39
|
+
MP_QQ: 'mp-qq',
|
|
40
|
+
MP_ALIPAY: 'mp-alipay',
|
|
41
|
+
MP_BAIDU: 'mp-baidu',
|
|
42
|
+
MP_TOUTIAO: 'mp-toutiao',
|
|
43
|
+
MP_KUAISHOU: 'mp-kuaishou',
|
|
44
|
+
MP_XHS: 'mp-xhs',
|
|
45
|
+
MP_JD: 'mp-jd',
|
|
46
|
+
H5: 'h5',
|
|
47
|
+
};
|
|
48
|
+
const ALL_PLATFORM = 'ALL';
|
|
49
|
+
const PLATFORMS_MP = [
|
|
50
|
+
PLATFORM_MAP.MP_WX,
|
|
51
|
+
PLATFORM_MAP.MP_QQ,
|
|
52
|
+
];
|
|
53
|
+
const PLATFORMS_ALL = [
|
|
54
|
+
PLATFORM_MAP.MP_WX,
|
|
55
|
+
PLATFORM_MAP.MP_QQ,
|
|
56
|
+
PLATFORM_MAP.H5,
|
|
57
|
+
];
|
|
58
|
+
const HTML_MAP = {
|
|
59
|
+
MP_WX: '.wxml',
|
|
60
|
+
MP_QQ: '.qml',
|
|
61
|
+
MP_ALIPAY: '.axml',
|
|
62
|
+
MP_JD: '.jxml',
|
|
63
|
+
};
|
|
64
|
+
const CSS_MAP = {
|
|
65
|
+
MP_WX: '.wxss',
|
|
66
|
+
MP_QQ: '.qss',
|
|
67
|
+
MP_ALIPAY: '.acss',
|
|
68
|
+
MP_JD: '.jxss',
|
|
69
|
+
};
|
|
70
|
+
const CSS_POSTFIX_MAP = Object.keys(CSS_MAP)
|
|
71
|
+
.reduce((acc, item) => {
|
|
72
|
+
acc[item] = CSS_MAP[item].slice(1);
|
|
73
|
+
return acc;
|
|
74
|
+
}, {});
|
|
75
|
+
const CDN_MAP = {
|
|
76
|
+
UNI_SIMPLE_ROUTER_BETA4: 'https://image-1251917893.file.myqcloud.com/igame/npm/uni-simple-router/uni-simple-router-2.0.8-beta-4.js',
|
|
77
|
+
UNI_SIMPLE_ROUTER: 'https://image-1251917893.file.myqcloud.com/igame/npm/uni-simple-router/uni-simple-router%402.0.8-beta.4-1.js',
|
|
78
|
+
AEGIS_WEB: 'https://image-1251917893.file.myqcloud.com/igame/npm/aegis-web/aegis.min.js',
|
|
79
|
+
AEGIS_WEB_V2: 'https://image-1251917893.file.myqcloud.com/igame/npm/aegis-web/v2.min.2.3.41.js',
|
|
80
|
+
AXIOS: 'https://image-1251917893.file.myqcloud.com/igame/npm/axios@0.18.0/dist/axios.min.js',
|
|
81
|
+
VUE_LAZY_LOAD: 'https://image-1251917893.file.myqcloud.com/igame/npm/vue-lazyload@1.3.3/vue-lazyload.js',
|
|
82
|
+
VUE_V2: 'https://image-1251917893.file.myqcloud.com/igame/npm/vue%402.6.10/dist/vue.runtime.min.js',
|
|
83
|
+
VUE_V3: 'https://image-1251917893.file.myqcloud.com/igame/npm/vue@3.3.6/vue.runtime.global.prod.js',
|
|
84
|
+
VUE_DEMI: 'https://image-1251917893.file.myqcloud.com/igame/npm/vue-demi@0.14.6/index.iife.min.js',
|
|
85
|
+
VUE_ROUTER_V3: 'https://image-1251917893.file.myqcloud.com/igame/npm/vue-router@3.5.2/dist/vue-router.min.js',
|
|
86
|
+
VUE_ROUTER_V4: 'https://image-1251917893.file.myqcloud.com/igame/npm/vue-router@4.2.5/vue-router.global.prod.js',
|
|
87
|
+
VUEX_V3: 'https://image-1251917893.file.myqcloud.com/igame/npm/vuex@3.0.1/dist/vuex.min.js',
|
|
88
|
+
VUEX_V4: 'https://image-1251917893.cos.ap-guangzhou.myqcloud.com/igame/npm/vuex%404.1.0/vuex.global.prod.js',
|
|
89
|
+
PINIA: 'https://image-1251917893.file.myqcloud.com/igame/npm/pinia@2.1.7/pinia.iife.min.js',
|
|
90
|
+
ELEMENT_PLUS: 'https://image-1251917893.file.myqcloud.com/igame/npm/element-plus/element-plus%402.7.8.js',
|
|
91
|
+
ELEMENT_PLUS_CSS: 'https://image-1251917893.file.myqcloud.com/igame/npm/element-plus/element-plus%402.7.8.css',
|
|
92
|
+
E_MONITOR: 'https://image-1251917893.file.myqcloud.com/igame/common/js/emonitor_custom_46f41566.js',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const DEFAULT_TRANSPILE_DEPENDENCIES = [
|
|
96
|
+
'pmd-merchant-ui',
|
|
97
|
+
'press-ui',
|
|
98
|
+
'press-plus',
|
|
99
|
+
'pmd-aegis',
|
|
100
|
+
'pmd-app-info',
|
|
101
|
+
'pmd-config',
|
|
102
|
+
'pmd-location',
|
|
103
|
+
'pmd-login',
|
|
104
|
+
'pmd-network',
|
|
105
|
+
'pmd-report',
|
|
106
|
+
'pmd-tools',
|
|
107
|
+
'pmd-types',
|
|
108
|
+
'pmd-widget',
|
|
109
|
+
'pmd-vue',
|
|
110
|
+
'pmd-jsapi',
|
|
111
|
+
'pmd-component',
|
|
112
|
+
'pmd-business',
|
|
113
|
+
'pmd-api',
|
|
114
|
+
'@zebra-ui/swiper',
|
|
115
|
+
];
|
|
116
|
+
const DEFAULT_ADAPTER_DIRS = [
|
|
117
|
+
'comm',
|
|
118
|
+
'common',
|
|
119
|
+
'component',
|
|
120
|
+
'live-component',
|
|
121
|
+
'logic',
|
|
122
|
+
'local-logic',
|
|
123
|
+
'local-component',
|
|
124
|
+
'login',
|
|
125
|
+
'pages',
|
|
126
|
+
'static',
|
|
127
|
+
'node-modules',
|
|
128
|
+
];
|
|
129
|
+
const AEGIS_EXTERNAL_SCRIPT_LINK = CDN_MAP.AEGIS_WEB;
|
|
130
|
+
const UNI_SIMPLE_ROUTER_SCRIPT_LINK = CDN_MAP.UNI_SIMPLE_ROUTER_BETA4;
|
|
131
|
+
const EXTERNAL_LINK_MAP = {
|
|
132
|
+
AXIOS: CDN_MAP.AXIOS,
|
|
133
|
+
VUE_LAZY_LOAD: CDN_MAP.VUE_LAZY_LOAD,
|
|
134
|
+
AEGIS_WEB: CDN_MAP.AEGIS_WEB,
|
|
135
|
+
UNI_SIMPLE_ROUTER: UNI_SIMPLE_ROUTER_SCRIPT_LINK,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const DEFAULT_KEYS = [
|
|
139
|
+
'UNI_APP_X',
|
|
140
|
+
'APP',
|
|
141
|
+
'APP_UVUE',
|
|
142
|
+
'APP_NVUE',
|
|
143
|
+
'APP_PLUS',
|
|
144
|
+
'APP_PLUS_NVUE',
|
|
145
|
+
'APP_VUE',
|
|
146
|
+
'APP_ANDROID',
|
|
147
|
+
'APP_IOS',
|
|
148
|
+
'APP_HARMONY',
|
|
149
|
+
'H5',
|
|
150
|
+
'MP',
|
|
151
|
+
'MP_360',
|
|
152
|
+
'MP_ALIPAY',
|
|
153
|
+
'MP_BAIDU',
|
|
154
|
+
'MP_QQ',
|
|
155
|
+
'MP_LARK',
|
|
156
|
+
'MP_TOUTIAO',
|
|
157
|
+
'MP_WEIXIN',
|
|
158
|
+
'MP_KUAISHOU',
|
|
159
|
+
'MP_JD',
|
|
160
|
+
'MP_XHS',
|
|
161
|
+
'QUICKAPP_NATIVE',
|
|
162
|
+
'QUICKAPP_WEBVIEW',
|
|
163
|
+
'QUICKAPP_WEBVIEW_HUAWEI',
|
|
164
|
+
'QUICKAPP_WEBVIEW_UNION',
|
|
165
|
+
'VUE2',
|
|
166
|
+
'VUE3',
|
|
167
|
+
'WEB',
|
|
168
|
+
];
|
|
169
|
+
const DEFAULT_CONTEXT_OBJECT = DEFAULT_KEYS.reduce((acc, key) => ({
|
|
170
|
+
...acc,
|
|
171
|
+
[key]: false,
|
|
172
|
+
}), {});
|
|
173
|
+
|
|
174
|
+
const TIP_STYLE_NAME = '@TIP_STYLE_NAME';
|
|
175
|
+
const DEFAULT_EXT_LIST = [
|
|
176
|
+
'scss',
|
|
177
|
+
'less',
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
function getRootDir() {
|
|
181
|
+
return process.cwd();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getAppDir() {
|
|
185
|
+
if (process.env.VUE_APP_DIR) {
|
|
186
|
+
return process.env.VUE_APP_DIR;
|
|
187
|
+
}
|
|
188
|
+
if (process.env.UNI_INPUT_DIR) {
|
|
189
|
+
return process.env.UNI_INPUT_DIR;
|
|
190
|
+
}
|
|
191
|
+
const dir = readEnvVariable('VUE_APP_DIR', path.join(getRootDir(), '.env.local'));
|
|
192
|
+
if (dir) {
|
|
193
|
+
return dir;
|
|
194
|
+
}
|
|
195
|
+
return '';
|
|
196
|
+
}
|
|
197
|
+
function getStyleName() {
|
|
198
|
+
const configPath = path.resolve(getRootDir(), 'src', getAppDir(), 'config.js');
|
|
199
|
+
let config = { styleName: '' };
|
|
200
|
+
if (fs.existsSync(configPath)) {
|
|
201
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
202
|
+
config = require(configPath);
|
|
203
|
+
}
|
|
204
|
+
const { styleName } = config;
|
|
205
|
+
return styleName;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function tryRemoveImport(source, removeImport = false) {
|
|
209
|
+
let res = source;
|
|
210
|
+
if (removeImport) {
|
|
211
|
+
res = res.replace(`src="${TIP_STYLE_NAME}"`, '').replace(`src='${TIP_STYLE_NAME}'`, '');
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
res = res.replace(TIP_STYLE_NAME, '');
|
|
215
|
+
}
|
|
216
|
+
return res;
|
|
217
|
+
}
|
|
218
|
+
function findValidFile(fileName, extList) {
|
|
219
|
+
for (const ext of extList) {
|
|
220
|
+
const file = `${fileName}.${ext}`;
|
|
221
|
+
if (fs.existsSync(file)) {
|
|
222
|
+
return [ext, file];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
function crossGameStyle({ source, options, dir, removeImport = false, }) {
|
|
228
|
+
if (!source.includes(TIP_STYLE_NAME)) {
|
|
229
|
+
return source;
|
|
230
|
+
}
|
|
231
|
+
const extList = options?.extList || DEFAULT_EXT_LIST;
|
|
232
|
+
let styleName = '';
|
|
233
|
+
// 使用 env.local 的样式 VUE_APP_DIR = module/ingame-nba,即为 nba
|
|
234
|
+
if (options?.styleName) {
|
|
235
|
+
styleName = options.styleName;
|
|
236
|
+
}
|
|
237
|
+
else if (getStyleName()) {
|
|
238
|
+
styleName = getStyleName();
|
|
239
|
+
}
|
|
240
|
+
if (Array.isArray(styleName)) {
|
|
241
|
+
if (styleName.length > 1) {
|
|
242
|
+
styleName = styleName.filter((item) => {
|
|
243
|
+
const cssAbsolutePath = `${dir}/css/${item}.scss`;
|
|
244
|
+
return fs.existsSync(cssAbsolutePath);
|
|
245
|
+
});
|
|
246
|
+
if (styleName.length > 1) {
|
|
247
|
+
const styleTags = styleName
|
|
248
|
+
.filter((item) => {
|
|
249
|
+
const cssAbsolutePath = `${dir}/css/${item}.scss`;
|
|
250
|
+
return removeImport ? fs.existsSync(cssAbsolutePath) : true;
|
|
251
|
+
})
|
|
252
|
+
.map(item => `.${item} {@import './css/${item}.scss';}`);
|
|
253
|
+
const res = tryRemoveImport(source, removeImport);
|
|
254
|
+
return res.replace(/<\/style>/, `</style>${['<style scoped lang="scss">', ...styleTags, '</style>'].join('')}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
styleName = styleName[0] || '';
|
|
258
|
+
}
|
|
259
|
+
const cssPath = `./css/${styleName}`;
|
|
260
|
+
const cssAbsolutePath = `${dir}/css/${styleName}`;
|
|
261
|
+
const found = findValidFile(cssAbsolutePath, extList);
|
|
262
|
+
if (found.length) {
|
|
263
|
+
const [ext] = found;
|
|
264
|
+
return source.replace(TIP_STYLE_NAME, `${cssPath}.${ext}`);
|
|
265
|
+
}
|
|
266
|
+
return tryRemoveImport(source, removeImport);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const scssLogger = {
|
|
270
|
+
warn(message) {
|
|
271
|
+
// Mute "Mixed Declarations" warning
|
|
272
|
+
if (message.includes('mixed-decls')) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// List all other warnings
|
|
276
|
+
console.warn(`▲ [WARNING]: ${message}`);
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
function getDeps(dir) {
|
|
281
|
+
const data = readFileSync(path$1.resolve(dir, 'package.json'), true);
|
|
282
|
+
return Object.keys({
|
|
283
|
+
...data.dependencies,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 方案二:IMAP IDLE 实时监听新邮件
|
|
289
|
+
*
|
|
290
|
+
* 通过 IMAP IDLE 命令保持长连接,实时监听收件箱中的新邮件到达。
|
|
291
|
+
* 适用于需要实时感知新邮件的场景,如:监听邮件后自动触发业务逻辑。
|
|
292
|
+
*
|
|
293
|
+
* 依赖安装:npm install imapflow mailparser
|
|
294
|
+
* 类型安装:npm install -D @types/mailparser
|
|
295
|
+
*
|
|
296
|
+
* 注意事项:
|
|
297
|
+
* 1. IDLE 连接可能因网络问题断开,内置自动重连机制
|
|
298
|
+
* 2. 部分邮箱服务器对 IDLE 连接有超时限制(通常 29 分钟),imapflow 会自动处理
|
|
299
|
+
* 3. 生产环境建议配合进程管理工具(如 pm2)使用
|
|
300
|
+
*/
|
|
301
|
+
// 初始化 HTML -> Markdown 转换器
|
|
302
|
+
const turndown = new TurndownService({
|
|
303
|
+
headingStyle: 'atx',
|
|
304
|
+
codeBlockStyle: 'fenced',
|
|
305
|
+
bulletListMarker: '-',
|
|
306
|
+
});
|
|
307
|
+
// ==================== 核心方法 ====================
|
|
308
|
+
/**
|
|
309
|
+
* 启动邮件监听(基于 IMAP IDLE)
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* const watcher = await watchEmails({
|
|
314
|
+
* host: 'imap.exmail.qq.com',
|
|
315
|
+
* user: 'yourname@company.com',
|
|
316
|
+
* password: 'your-app-password',
|
|
317
|
+
* onNewEmail: (email) => {
|
|
318
|
+
* console.log('📬 收到新邮件:', email.subject);
|
|
319
|
+
* // 触发你的业务逻辑,比如推送到企微群
|
|
320
|
+
* },
|
|
321
|
+
* onError: (err) => {
|
|
322
|
+
* console.error('监听出错:', err.message);
|
|
323
|
+
* },
|
|
324
|
+
* });
|
|
325
|
+
*
|
|
326
|
+
* // 需要停止时
|
|
327
|
+
* // await watcher.stop();
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
async function watchEmails(options) {
|
|
331
|
+
const { host, port = 143, user, password, mailbox = 'INBOX', onNewEmail, onError, onConnected, onDisconnected, autoReconnect = true, reconnectInterval = 5000, maxReconnectAttempts = Infinity, debug = false, } = options;
|
|
332
|
+
let client = null;
|
|
333
|
+
let connected = false;
|
|
334
|
+
let stopped = false;
|
|
335
|
+
let reconnectAttempts = 0;
|
|
336
|
+
let reconnectTimer = null;
|
|
337
|
+
/**
|
|
338
|
+
* 创建 IMAP 客户端并开始监听
|
|
339
|
+
*/
|
|
340
|
+
async function connect() {
|
|
341
|
+
if (stopped)
|
|
342
|
+
return;
|
|
343
|
+
const secure = options.secure ?? (port === 993);
|
|
344
|
+
client = new ImapFlow({
|
|
345
|
+
host,
|
|
346
|
+
port,
|
|
347
|
+
secure,
|
|
348
|
+
auth: { user, pass: password },
|
|
349
|
+
logger: debug ? console : false,
|
|
350
|
+
tls: {
|
|
351
|
+
rejectUnauthorized: false, // 内网邮箱服务器可能使用自签名证书
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
try {
|
|
355
|
+
await client.connect();
|
|
356
|
+
connected = true;
|
|
357
|
+
reconnectAttempts = 0;
|
|
358
|
+
console.log(`[watchEmails] 已连接到 ${host}:${port},用户: ${user}`);
|
|
359
|
+
onConnected?.();
|
|
360
|
+
// 打开邮箱
|
|
361
|
+
await client.mailboxOpen(mailbox);
|
|
362
|
+
console.log(`[watchEmails] 已打开 ${mailbox},开始监听新邮件...`);
|
|
363
|
+
// 监听新邮件事件
|
|
364
|
+
client.on('exists', (data) => {
|
|
365
|
+
console.log(`[watchEmails] 📬 检测到新邮件! 当前共 ${data.count} 封 (之前 ${data.prevCount} 封)`);
|
|
366
|
+
// 计算新增邮件数量
|
|
367
|
+
const newCount = data.count - data.prevCount;
|
|
368
|
+
if (newCount <= 0)
|
|
369
|
+
return;
|
|
370
|
+
// 使用 void 包装异步操作,避免 Promise 返回到 void 回调中
|
|
371
|
+
void (async () => {
|
|
372
|
+
try {
|
|
373
|
+
// 获取最新的邮件(按 seq 号获取)
|
|
374
|
+
const startSeq = data.prevCount + 1;
|
|
375
|
+
const range = newCount === 1 ? `${startSeq}` : `${startSeq}:${data.count}`;
|
|
376
|
+
for await (const msg of client.fetch(range, { source: true })) {
|
|
377
|
+
try {
|
|
378
|
+
if (!msg.source) {
|
|
379
|
+
console.warn(`[watchEmails] 邮件 UID=${msg.uid} 的 source 为空,跳过`);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const parsed = await simpleParser(msg.source);
|
|
383
|
+
// 将 HTML 转换为 Markdown,如果没有 HTML 则使用纯文本
|
|
384
|
+
let markdown = '';
|
|
385
|
+
if (parsed.html) {
|
|
386
|
+
markdown = turndown.turndown(parsed.html);
|
|
387
|
+
}
|
|
388
|
+
else if (parsed.text) {
|
|
389
|
+
markdown = parsed.text;
|
|
390
|
+
}
|
|
391
|
+
const email = {
|
|
392
|
+
uid: msg.uid,
|
|
393
|
+
subject: parsed.subject || '(无主题)',
|
|
394
|
+
from: parsed.from?.text || '',
|
|
395
|
+
to: Array.isArray(parsed.to) ? parsed.to.map(addr => addr.text).join(', ') : (parsed.to?.text || ''),
|
|
396
|
+
date: parsed.date,
|
|
397
|
+
text: parsed.text,
|
|
398
|
+
html: parsed.html || false,
|
|
399
|
+
markdown,
|
|
400
|
+
attachments: (parsed.attachments || []).map(att => ({
|
|
401
|
+
filename: att.filename || '未命名',
|
|
402
|
+
contentType: att.contentType || 'application/octet-stream',
|
|
403
|
+
size: att.size,
|
|
404
|
+
})),
|
|
405
|
+
};
|
|
406
|
+
// 调用用户回调(支持异步)
|
|
407
|
+
await onNewEmail(email);
|
|
408
|
+
}
|
|
409
|
+
catch (parseErr) {
|
|
410
|
+
console.error('[watchEmails] 解析邮件失败:', parseErr);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch (fetchErr) {
|
|
415
|
+
console.error('[watchEmails] 获取新邮件失败:', fetchErr);
|
|
416
|
+
onError?.(fetchErr);
|
|
417
|
+
}
|
|
418
|
+
})();
|
|
419
|
+
});
|
|
420
|
+
// 监听连接关闭事件(用于自动重连)
|
|
421
|
+
client.on('close', () => {
|
|
422
|
+
connected = false;
|
|
423
|
+
console.log('[watchEmails] 连接已关闭');
|
|
424
|
+
onDisconnected?.();
|
|
425
|
+
if (!stopped && autoReconnect) {
|
|
426
|
+
scheduleReconnect();
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
client.on('error', (err) => {
|
|
430
|
+
console.error('[watchEmails] 连接错误:', err.message);
|
|
431
|
+
onError?.(err);
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
connected = false;
|
|
436
|
+
console.error('[watchEmails] 连接失败:', error.message);
|
|
437
|
+
onError?.(error);
|
|
438
|
+
if (!stopped && autoReconnect) {
|
|
439
|
+
scheduleReconnect();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* 安排自动重连
|
|
445
|
+
*/
|
|
446
|
+
function scheduleReconnect() {
|
|
447
|
+
if (stopped)
|
|
448
|
+
return;
|
|
449
|
+
reconnectAttempts += 1;
|
|
450
|
+
if (reconnectAttempts > maxReconnectAttempts) {
|
|
451
|
+
console.error(`[watchEmails] 已达最大重连次数 (${maxReconnectAttempts}),停止重连`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const delay = Math.min(reconnectInterval * reconnectAttempts, 60000); // 最大 60s
|
|
455
|
+
console.log(`[watchEmails] 将在 ${delay}ms 后重连 (第 ${reconnectAttempts} 次)...`);
|
|
456
|
+
reconnectTimer = setTimeout(() => {
|
|
457
|
+
reconnectTimer = null;
|
|
458
|
+
void (async () => {
|
|
459
|
+
try {
|
|
460
|
+
await connect();
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
console.error('[watchEmails] 重连失败:', err);
|
|
464
|
+
}
|
|
465
|
+
})();
|
|
466
|
+
}, delay);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* 停止监听
|
|
470
|
+
*/
|
|
471
|
+
async function stop() {
|
|
472
|
+
stopped = true;
|
|
473
|
+
if (reconnectTimer) {
|
|
474
|
+
clearTimeout(reconnectTimer);
|
|
475
|
+
reconnectTimer = null;
|
|
476
|
+
}
|
|
477
|
+
if (client) {
|
|
478
|
+
try {
|
|
479
|
+
await client.logout();
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
// 忽略断开时的错误
|
|
483
|
+
}
|
|
484
|
+
client = null;
|
|
485
|
+
}
|
|
486
|
+
connected = false;
|
|
487
|
+
console.log('[watchEmails] 监听已停止');
|
|
488
|
+
}
|
|
489
|
+
// 启动初始连接
|
|
490
|
+
await connect();
|
|
491
|
+
return {
|
|
492
|
+
stop,
|
|
493
|
+
isConnected: () => connected,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function getImportOrderRule() {
|
|
498
|
+
return {
|
|
499
|
+
'import/order': [
|
|
500
|
+
'error',
|
|
501
|
+
{
|
|
502
|
+
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
|
503
|
+
'newlines-between': 'always-and-inside-groups',
|
|
504
|
+
alphabetize: {
|
|
505
|
+
order: 'asc',
|
|
506
|
+
caseInsensitive: true,
|
|
507
|
+
},
|
|
508
|
+
pathGroups: [
|
|
509
|
+
{
|
|
510
|
+
pattern: 'vue',
|
|
511
|
+
group: 'external',
|
|
512
|
+
position: 'before',
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
pattern: 'vite',
|
|
516
|
+
group: 'external',
|
|
517
|
+
position: 'before',
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
pathGroupsExcludedImportTypes: ['builtin'],
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function getParent(path, level) {
|
|
527
|
+
if (!level) {
|
|
528
|
+
return path.parent;
|
|
529
|
+
}
|
|
530
|
+
return getParent(path.parentPath, level - 1);
|
|
531
|
+
}
|
|
532
|
+
function findDependencies(content) {
|
|
533
|
+
const parser = require('@babel/parser');
|
|
534
|
+
const traverse = require('@babel/traverse').default;
|
|
535
|
+
const sourceList = [];
|
|
536
|
+
const ast = parser.parse(content, {
|
|
537
|
+
// 不加这个配置,报错:SyntaxError: 'import' and 'export' may appear only with 'sourceType: "module"'
|
|
538
|
+
sourceType: 'module',
|
|
539
|
+
plugins: ['typescript'],
|
|
540
|
+
});
|
|
541
|
+
traverse(ast, {
|
|
542
|
+
CallExpression(path) {
|
|
543
|
+
if (path.node.callee.name === 'require') {
|
|
544
|
+
if (path.node.arguments[0].type === 'StringLiteral') {
|
|
545
|
+
sourceList.push(path.node.arguments[0].value);
|
|
546
|
+
}
|
|
547
|
+
if (getParent(path, 0).type === 'ExpressionStatement'
|
|
548
|
+
&& getParent(path, 1).type === 'BlockStatement'
|
|
549
|
+
&& getParent(path, 2).type === 'ObjectMethod'
|
|
550
|
+
&& getParent(path, 3).type === 'ObjectExpression'
|
|
551
|
+
&& getParent(path, 4)?.type === 'ObjectProperty'
|
|
552
|
+
&& getParent(path, 4)?.key?.name === 'components'
|
|
553
|
+
&& getParent(path, 5)?.type === 'ObjectExpression'
|
|
554
|
+
&& getParent(path, 6)?.type === 'ExportDefaultDeclaration'
|
|
555
|
+
&& path.node.arguments[0].type === 'ArrayExpression') {
|
|
556
|
+
sourceList.push(path.node.arguments[0].elements[0].value);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
ImportDeclaration(path) {
|
|
561
|
+
sourceList.push(path.node.source.value);
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
return sourceList;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* 字体项目 CDN 地址
|
|
569
|
+
*/
|
|
570
|
+
const FONT_PROJECT_CDN = 'https://image-1251917893.cos.ap-guangzhou.myqcloud.com/pmd-font/font-project.json';
|
|
571
|
+
/**
|
|
572
|
+
* 字体项目 COS 基础配置
|
|
573
|
+
*/
|
|
574
|
+
const FONT_PROJECT_COS_BASE = {
|
|
575
|
+
bucket: 'image-1251917893',
|
|
576
|
+
region: 'ap-guangzhou',
|
|
577
|
+
};
|
|
578
|
+
/**
|
|
579
|
+
* 字体项目 Webhook URL
|
|
580
|
+
*/
|
|
581
|
+
const FONT_PROJECT_WEBHOOK_URL = '9f673531-788b-4780-be72-93b16a62e3eb';
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* 获取上传图片的文件路径
|
|
585
|
+
* @param cdn - CDN 域名
|
|
586
|
+
* @returns 格式化的文件路径,包含年月信息
|
|
587
|
+
*/
|
|
588
|
+
function getUploadImageFilePath(cdn) {
|
|
589
|
+
const { cosPrefix } = getUploadCosConfig(cdn);
|
|
590
|
+
const date = new Date();
|
|
591
|
+
const year = date.getFullYear();
|
|
592
|
+
const month = date.getMonth() + 1;
|
|
593
|
+
return `${cosPrefix}/${year}/${month}`;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* 从 COS 基础域名中提取 region
|
|
597
|
+
* @param cosBaseDomain - COS 基础域名
|
|
598
|
+
* @returns region 字符串
|
|
599
|
+
*/
|
|
600
|
+
function getCosRegion(cosBaseDomain = '') {
|
|
601
|
+
return cosBaseDomain.split('.')[1];
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* 获取上传 COS 配置
|
|
605
|
+
* @param cdn - CDN 域名
|
|
606
|
+
* @returns COS 配置对象,如果 CDN 无效则返回空对象
|
|
607
|
+
*/
|
|
608
|
+
function getUploadCosConfig(cdn) {
|
|
609
|
+
if (!cdn) {
|
|
610
|
+
return {};
|
|
611
|
+
}
|
|
612
|
+
const cdnMap = getCdnList().reduce((acc, item) => ({
|
|
613
|
+
...acc,
|
|
614
|
+
[item.cdnDomain]: item,
|
|
615
|
+
}), {});
|
|
616
|
+
const cur = cdnMap[cdn];
|
|
617
|
+
if (!cur) {
|
|
618
|
+
return {};
|
|
619
|
+
}
|
|
620
|
+
const { secretId, secretKey, cosBaseDomain, cosBucketName, cosAppId } = cur;
|
|
621
|
+
const region = getCosRegion(cosBaseDomain);
|
|
622
|
+
return {
|
|
623
|
+
secretId,
|
|
624
|
+
secretKey,
|
|
625
|
+
bucket: `${cosBucketName}-${cosAppId}`,
|
|
626
|
+
region,
|
|
627
|
+
cosPrefix: 'next-svr/images',
|
|
628
|
+
maxSize: 1 * 1024 * 1024, // 1M
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* 解析图片记录,将 COS URL 转换为 CDN URL
|
|
633
|
+
* @param image - 图片记录对象
|
|
634
|
+
* @returns 解析后的图片记录,包含 parsedUrl
|
|
635
|
+
*/
|
|
636
|
+
const parseImage = (image) => ({
|
|
637
|
+
...image,
|
|
638
|
+
parsedUrl: toCdnUrl(image.url),
|
|
639
|
+
});
|
|
640
|
+
const cdnMap = new Map(getCdnList().map(item => [item.cosDomain, item.cdnDomain]));
|
|
641
|
+
/**
|
|
642
|
+
* 从 URL 中提取域名
|
|
643
|
+
* @param url - 输入的 URL
|
|
644
|
+
* @returns 域名字符串,如果无法提取则返回空字符串
|
|
645
|
+
*/
|
|
646
|
+
function extractDomain(url) {
|
|
647
|
+
if (!url) {
|
|
648
|
+
return '';
|
|
649
|
+
}
|
|
650
|
+
// 处理 https:// 或 http:// 开头的链接
|
|
651
|
+
const protocolMatch = url.match(/^https?:\/\/([^/]+)/);
|
|
652
|
+
if (protocolMatch) {
|
|
653
|
+
return protocolMatch[1];
|
|
654
|
+
}
|
|
655
|
+
// 处理 // 开头的协议相对链接
|
|
656
|
+
const relativeMatch = url.match(/^\/\/([^/]+)/);
|
|
657
|
+
if (relativeMatch) {
|
|
658
|
+
return relativeMatch[1];
|
|
659
|
+
}
|
|
660
|
+
// 处理纯域名格式(不带协议前缀),如 a.cos.ap-guangzhou.myqcloud.com 或 a.cos.ap-guangzhou.myqcloud.com/path
|
|
661
|
+
// 域名特征:包含至少一个点,且第一段不以 / 开头
|
|
662
|
+
const pureDomainMatch = url.match(/^([a-zA-Z0-9][\w.-]*\.[a-zA-Z]{2,}(?:\.[a-zA-Z]{2,})*)/);
|
|
663
|
+
if (pureDomainMatch) {
|
|
664
|
+
return pureDomainMatch[1];
|
|
665
|
+
}
|
|
666
|
+
return '';
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* 将 COS URL 转换为 CDN URL
|
|
670
|
+
* 支持 https://、http://、//、纯域名 等多种链接格式
|
|
671
|
+
* @param inUrl - 输入的 URL
|
|
672
|
+
* @returns 转换后的 CDN URL
|
|
673
|
+
* @example
|
|
674
|
+
* toCdnUrl('https://xxx.cos.ap-guangzhou.myqcloud.com/path/to/file')
|
|
675
|
+
* toCdnUrl('//xxx.cos.ap-guangzhou.myqcloud.com/path/to/file')
|
|
676
|
+
* toCdnUrl('http://xxx.cos.ap-guangzhou.myqcloud.com/path/to/file')
|
|
677
|
+
* toCdnUrl('xxx.cos.ap-guangzhou.myqcloud.com/path/to/file')
|
|
678
|
+
* toCdnUrl('xxx.cos.ap-guangzhou.myqcloud.com')
|
|
679
|
+
*/
|
|
680
|
+
function toCdnUrl(inUrl) {
|
|
681
|
+
if (!inUrl) {
|
|
682
|
+
return inUrl;
|
|
683
|
+
}
|
|
684
|
+
const domain = extractDomain(inUrl);
|
|
685
|
+
if (!domain) {
|
|
686
|
+
return inUrl;
|
|
687
|
+
}
|
|
688
|
+
if (cdnMap.has(domain)) {
|
|
689
|
+
return inUrl.replace(domain, cdnMap.get(domain));
|
|
690
|
+
}
|
|
691
|
+
return inUrl;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* 将 COS URL 转换为 CDN URL
|
|
695
|
+
* @deprecated 请使用 toCdnUrl 代替
|
|
696
|
+
* @param inUrl - 输入的 URL
|
|
697
|
+
* @returns 转换后的 CDN URL
|
|
698
|
+
*/
|
|
699
|
+
const getOneCdnUrl = toCdnUrl;
|
|
700
|
+
/**
|
|
701
|
+
* 中国大陆 CDN 列表
|
|
702
|
+
*/
|
|
703
|
+
const MAINLAND_CDN_LIST = [
|
|
704
|
+
'image-1251917893.file.myqcloud.com',
|
|
705
|
+
];
|
|
706
|
+
/**
|
|
707
|
+
* 根据 CDN 获取推送 URL 缓存区域
|
|
708
|
+
* @param cdn - CDN 域名
|
|
709
|
+
* @returns 缓存区域,中国大陆返回 'mainland',否则返回 'overseas'
|
|
710
|
+
*/
|
|
711
|
+
const getPushUrlCacheArea = (cdn) => (MAINLAND_CDN_LIST.includes(cdn) ? 'mainland' : 'overseas');
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* 文件上传核心处理函数
|
|
715
|
+
* @param options - 上传选项
|
|
716
|
+
* @returns 上传结果
|
|
717
|
+
*/
|
|
718
|
+
async function uploadFilesCore({ file, originName, useOriginFilename, newFileKey, staffName, parsedDir, uploadFile, cdn, cos, uploadCallBack, fromMcp = false, mcpDB, mcpName, mcpVersion, imageDB, operationTool, }) {
|
|
719
|
+
const uploadError = await uploadCallBack();
|
|
720
|
+
if (uploadError) {
|
|
721
|
+
return uploadError;
|
|
722
|
+
}
|
|
723
|
+
const url = `https://${cos.bucket}.cos.${cos.region}.myqcloud.com/${newFileKey}`;
|
|
724
|
+
await imageDB.insert({
|
|
725
|
+
fileKey: newFileKey,
|
|
726
|
+
url,
|
|
727
|
+
size: file.size,
|
|
728
|
+
mimetype: file.mimetype,
|
|
729
|
+
originalname: originName,
|
|
730
|
+
createTime: Date.now(),
|
|
731
|
+
staffName,
|
|
732
|
+
dir: parsedDir,
|
|
733
|
+
uploadFile,
|
|
734
|
+
cdn,
|
|
735
|
+
});
|
|
736
|
+
try {
|
|
737
|
+
purgeUrlCache({
|
|
738
|
+
secretId: cos.secretId,
|
|
739
|
+
secretKey: cos.secretKey,
|
|
740
|
+
urls: [toCdnUrl(url)],
|
|
741
|
+
area: getPushUrlCacheArea(cdn),
|
|
742
|
+
})
|
|
743
|
+
.then((res) => {
|
|
744
|
+
console.log('[purgeUrlCache] res: ', res?.data);
|
|
745
|
+
})
|
|
746
|
+
.catch((err) => {
|
|
747
|
+
console.log('[purgeUrlCache] err: ', err);
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
catch (err) {
|
|
751
|
+
// 忽略错误
|
|
752
|
+
}
|
|
753
|
+
const recordList = [
|
|
754
|
+
`原始名称:${originName}`,
|
|
755
|
+
`目录:${parsedDir}`,
|
|
756
|
+
`是否使用原始文件名:${useOriginFilename ? '是' : '否'}`,
|
|
757
|
+
`CDN:${cdn}`,
|
|
758
|
+
];
|
|
759
|
+
await operationTool.addByMapParam({
|
|
760
|
+
staffName,
|
|
761
|
+
type: uploadFile ? 'UPLOAD_FILE' : 'UPLOAD_IMAGE',
|
|
762
|
+
desc: [
|
|
763
|
+
...recordList,
|
|
764
|
+
`MCP:${fromMcp ? '是' : '否'}`,
|
|
765
|
+
].join(','),
|
|
766
|
+
status: 'success',
|
|
767
|
+
extra: {
|
|
768
|
+
fileKey: newFileKey,
|
|
769
|
+
url,
|
|
770
|
+
size: file.size,
|
|
771
|
+
mimetype: file.mimetype,
|
|
772
|
+
originalname: originName,
|
|
773
|
+
dir: parsedDir,
|
|
774
|
+
useOriginFilename,
|
|
775
|
+
uploadFile,
|
|
776
|
+
cdn,
|
|
777
|
+
fromMcp,
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
if (typeof mcpDB?.add === 'function') {
|
|
781
|
+
const mcpContent = recordList.join(',');
|
|
782
|
+
await mcpDB.add(mcpName || 'upload-mcp', staffName, mcpContent, mcpVersion);
|
|
783
|
+
}
|
|
784
|
+
return { r: 0, msg: '上传成功', url, cdnUrl: toCdnUrl(url) };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* COS 同步到 Git 的远程流水线 ID
|
|
789
|
+
*/
|
|
790
|
+
const COS_TO_GIT_REMOTE_PIPELINE_ID = 'df18b1dc954e49c98271c531a0f40812';
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* 获取所有字体项目配置
|
|
794
|
+
* @returns 包含字体项目数据的 Promise
|
|
795
|
+
*/
|
|
796
|
+
function genFontProjects() {
|
|
797
|
+
return axios.get(`${FONT_PROJECT_CDN}?v=${Date.now()}`);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* 创建或更新字体项目
|
|
801
|
+
* @param options - 创建/更新选项
|
|
802
|
+
* @returns 操作结果
|
|
803
|
+
*/
|
|
804
|
+
async function createFontProjectOrUpdate({ projectNameZN = '', projectName = '', fontFileName = '', validationText = '', cdn = '', cos, staffName = '', isCreate, tags = [], }) {
|
|
805
|
+
const { data } = await genFontProjects();
|
|
806
|
+
if (data[projectName] && isCreate) {
|
|
807
|
+
return {
|
|
808
|
+
error: 'project already exists',
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
const createInfo = isCreate || !data[projectName]?.creator ? {
|
|
812
|
+
creator: staffName,
|
|
813
|
+
createTime: Date.now(),
|
|
814
|
+
} : {};
|
|
815
|
+
data[projectName] = {
|
|
816
|
+
...data[projectName],
|
|
817
|
+
projectNameZN,
|
|
818
|
+
projectName,
|
|
819
|
+
fontFileName,
|
|
820
|
+
validationText,
|
|
821
|
+
cdn,
|
|
822
|
+
tags,
|
|
823
|
+
...createInfo,
|
|
824
|
+
updateTime: Date.now(),
|
|
825
|
+
};
|
|
826
|
+
const key = FONT_PROJECT_CDN.replace(/^https:\/\/[^/]+\//, '');
|
|
827
|
+
console.log('[createFontProjectOrUpdate] data: ', { data, key });
|
|
828
|
+
try {
|
|
829
|
+
await uploadCOSStreamFile({
|
|
830
|
+
secretId: cos.secretId,
|
|
831
|
+
secretKey: cos.secretKey,
|
|
832
|
+
bucket: cos.bucket,
|
|
833
|
+
region: cos.region,
|
|
834
|
+
key,
|
|
835
|
+
file: {
|
|
836
|
+
buffer: Buffer.from(JSON.stringify(data, null, 2)),
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
await purgeUrlCache({
|
|
840
|
+
secretId: cos.secretId,
|
|
841
|
+
secretKey: cos.secretKey,
|
|
842
|
+
urls: [toCdnUrl(FONT_PROJECT_CDN)],
|
|
843
|
+
})
|
|
844
|
+
.then((res) => {
|
|
845
|
+
console.log('[purgeUrlCache] res: ', res?.data);
|
|
846
|
+
})
|
|
847
|
+
.catch((err) => {
|
|
848
|
+
console.log('[purgeUrlCache] err: ', err);
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
catch (e) {
|
|
852
|
+
return {
|
|
853
|
+
error: e.message,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
857
|
+
isCreate,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function parseQuote(str = '') {
|
|
862
|
+
return str.replace(/'/g, '"');
|
|
863
|
+
}
|
|
864
|
+
function getVersionCode(versionName) {
|
|
865
|
+
let author = '';
|
|
866
|
+
let branch = '';
|
|
867
|
+
try {
|
|
868
|
+
author = getGitAuthor(false) || '';
|
|
869
|
+
branch = getGitCurBranch() || '';
|
|
870
|
+
}
|
|
871
|
+
catch (err) { }
|
|
872
|
+
const versionInfo = {
|
|
873
|
+
time: timeStampFormat(Date.now(), 'yyyy-MM-dd hh:mm:ss'),
|
|
874
|
+
author,
|
|
875
|
+
branch,
|
|
876
|
+
netEnv: process.env.NET_ENV || '',
|
|
877
|
+
};
|
|
878
|
+
let code = '';
|
|
879
|
+
if (versionName) {
|
|
880
|
+
code = `
|
|
881
|
+
window.${versionName} = {
|
|
882
|
+
time: '${versionInfo.time}',
|
|
883
|
+
author: '${versionInfo.author}',
|
|
884
|
+
branch: '${versionInfo.branch}',
|
|
885
|
+
netEnv: '${versionInfo.netEnv}',
|
|
886
|
+
}
|
|
887
|
+
`;
|
|
888
|
+
return code;
|
|
889
|
+
}
|
|
890
|
+
code = `
|
|
891
|
+
console.info('[system]', '');
|
|
892
|
+
console.info('[system]', 'Build Time: ${versionInfo.time || ''}');
|
|
893
|
+
console.info('[system]', 'Build Author: ${versionInfo.author || ''}');
|
|
894
|
+
console.info('[system]', 'Build Branch: ${versionInfo.branch || ''}');
|
|
895
|
+
console.info('[system]', 'Build Net Env: ${versionInfo.netEnv || ''}');
|
|
896
|
+
`;
|
|
897
|
+
return code;
|
|
898
|
+
}
|
|
899
|
+
function getCommitCode(versionName) {
|
|
900
|
+
let commitInfo = {};
|
|
901
|
+
try {
|
|
902
|
+
commitInfo = getGitCommitInfo();
|
|
903
|
+
}
|
|
904
|
+
catch (err) { }
|
|
905
|
+
if (commitInfo.timeStamp) {
|
|
906
|
+
commitInfo.date = timeStampFormat(commitInfo.timeStamp, 'yyyy-MM-dd hh:mm:ss');
|
|
907
|
+
}
|
|
908
|
+
let code = '';
|
|
909
|
+
if (versionName) {
|
|
910
|
+
code = `
|
|
911
|
+
window.${versionName} = {
|
|
912
|
+
message: '${parseQuote(commitInfo.message)}',
|
|
913
|
+
author: '${commitInfo.author}',
|
|
914
|
+
date: '${commitInfo.date}',
|
|
915
|
+
hash: '${commitInfo.hash}',
|
|
916
|
+
}
|
|
917
|
+
`;
|
|
918
|
+
return code;
|
|
919
|
+
}
|
|
920
|
+
code = `
|
|
921
|
+
console.info('[system]', '');
|
|
922
|
+
console.info('[system]', 'Last Commit Message: ${parseQuote(commitInfo.message) || ''}');
|
|
923
|
+
console.info('[system]', 'Last Commit Author: ${commitInfo.author || ''}');
|
|
924
|
+
console.info('[system]', 'Last Commit Time: ${commitInfo.date || ''}');
|
|
925
|
+
console.info('[system]', 'Last Commit Hash: ${commitInfo.hash || ''}');
|
|
926
|
+
`;
|
|
927
|
+
return code;
|
|
928
|
+
}
|
|
929
|
+
function getMpVersionCode() {
|
|
930
|
+
return `
|
|
931
|
+
var uni = (typeof wx !== 'undefined' && wx)
|
|
932
|
+
|| (typeof qq !== 'undefined' && qq)
|
|
933
|
+
|| (typeof swan !== 'undefined' && swan)
|
|
934
|
+
|| (typeof jd !== 'undefined' && jd)
|
|
935
|
+
|| (typeof tt !== 'undefined' && tt)
|
|
936
|
+
|| (typeof kuaishou !== 'undefined' && kuaishou)
|
|
937
|
+
|| (typeof xhs !== 'undefined' && xhs)
|
|
938
|
+
|| (typeof my !== 'undefined' && my);
|
|
939
|
+
|
|
940
|
+
var miniProgram = (uni.getAccountInfoSync && uni.getAccountInfoSync().miniProgram) || {};
|
|
941
|
+
var { envVersion = '', version = '' } = miniProgram;
|
|
942
|
+
function getSystemInfo() {
|
|
943
|
+
return (uni.getSystemInfoSync && uni.getSystemInfoSync()) || {};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
var { pixelRatio } = (uni.getWindowInfo && uni.getWindowInfo()) || getSystemInfo();
|
|
947
|
+
var { brand, model, pixelRatio } = (uni.getDeviceInfo && uni.getDeviceInfo()) || getSystemInfo();
|
|
948
|
+
var envVersionMap = {
|
|
949
|
+
develop: '开发版',
|
|
950
|
+
trial: '体验版',
|
|
951
|
+
release: '正式版'
|
|
952
|
+
};
|
|
953
|
+
var versionDesc = \`\${envVersion}(\${envVersionMap[envVersion] || ''})\`;
|
|
954
|
+
|
|
955
|
+
console.info('[system]', '');
|
|
956
|
+
console.info('[system]', \`Env Version:\${envVersion ? versionDesc : ''}\`);
|
|
957
|
+
console.info('[system]', \`Version:\${version}\`);
|
|
958
|
+
|
|
959
|
+
console.info('[system]', '');
|
|
960
|
+
console.info('[system]', \`Brand:\${brand}, \${model}\`);
|
|
961
|
+
console.info('[system]', \`PixelRatio:\${pixelRatio}\`);
|
|
962
|
+
`;
|
|
963
|
+
}
|
|
964
|
+
function getMpInsertCode() {
|
|
965
|
+
const insertCode = `
|
|
966
|
+
try {
|
|
967
|
+
setTimeout(() => {
|
|
968
|
+
${getVersionCode()}
|
|
969
|
+
${getCommitCode()}
|
|
970
|
+
${getMpVersionCode()}
|
|
971
|
+
}, 2000);
|
|
972
|
+
} catch(err) {}
|
|
973
|
+
`;
|
|
974
|
+
return insertCode;
|
|
975
|
+
}
|
|
976
|
+
function getGenVersionPluginOptions(options) {
|
|
977
|
+
const buildName = options?.buildName || '';
|
|
978
|
+
const commitName = options?.commitName || '';
|
|
979
|
+
const delay = options?.delay === undefined ? 10 : options?.delay;
|
|
980
|
+
return {
|
|
981
|
+
buildName,
|
|
982
|
+
commitName,
|
|
983
|
+
delay,
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
function getWebInsertCode(options) {
|
|
987
|
+
const { buildName, commitName, delay } = getGenVersionPluginOptions(options || {});
|
|
988
|
+
return `
|
|
989
|
+
<script>
|
|
990
|
+
try {
|
|
991
|
+
setTimeout(() => {
|
|
992
|
+
${getVersionCode(buildName)}
|
|
993
|
+
${getCommitCode(commitName)}
|
|
994
|
+
}, ${delay});
|
|
995
|
+
} catch(err) {}
|
|
996
|
+
</script>
|
|
997
|
+
`;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function checkH5() {
|
|
1001
|
+
return process.env.VUE_APP_PLATFORM === 'h5';
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
1005
|
+
const ROOT_NAME = 'MAIN';
|
|
1006
|
+
function saveJsonToLog(content, file, needLog = true) {
|
|
1007
|
+
if (!needLog)
|
|
1008
|
+
return;
|
|
1009
|
+
createLogDir();
|
|
1010
|
+
const filePath = `./log/${file}`;
|
|
1011
|
+
let beforeContent = [];
|
|
1012
|
+
let newContent = [{
|
|
1013
|
+
logTime: timeStampFormat(Date.now(), 'yyyy-MM-dd hh:mm:ss'),
|
|
1014
|
+
data: content,
|
|
1015
|
+
}];
|
|
1016
|
+
if (fs.existsSync(filePath)) {
|
|
1017
|
+
try {
|
|
1018
|
+
beforeContent = readFileSync(filePath, true).logList || [];
|
|
1019
|
+
}
|
|
1020
|
+
catch (err) {
|
|
1021
|
+
beforeContent = [];
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (beforeContent && Array.isArray(beforeContent)) {
|
|
1025
|
+
newContent.push(...beforeContent);
|
|
1026
|
+
}
|
|
1027
|
+
newContent = newContent.slice(0, 10);
|
|
1028
|
+
try {
|
|
1029
|
+
fs.writeFile(filePath, JSON.stringify({ logList: newContent }, null, 2), {
|
|
1030
|
+
encoding: 'utf-8',
|
|
1031
|
+
}, () => { });
|
|
1032
|
+
}
|
|
1033
|
+
catch (err) {
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function createLogDir() {
|
|
1037
|
+
if (!fs.existsSync('./log')) {
|
|
1038
|
+
fs.mkdirSync('./log');
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
const normalizePath = (path) => (isWindows() ? path.replace(/\\/g, '/') : path);
|
|
1042
|
+
function updateAssetSource(assets, key, source) {
|
|
1043
|
+
assets[key] = {
|
|
1044
|
+
source() {
|
|
1045
|
+
return source;
|
|
1046
|
+
},
|
|
1047
|
+
size() {
|
|
1048
|
+
return source.length;
|
|
1049
|
+
},
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function removeFirstSlash(key) {
|
|
1053
|
+
if (key.startsWith('/')) {
|
|
1054
|
+
return key.slice(1);
|
|
1055
|
+
}
|
|
1056
|
+
return key;
|
|
1057
|
+
}
|
|
1058
|
+
function sortStringList(list) {
|
|
1059
|
+
list.sort((a, b) => {
|
|
1060
|
+
if (a > b)
|
|
1061
|
+
return 1;
|
|
1062
|
+
if (a < b)
|
|
1063
|
+
return -1;
|
|
1064
|
+
return 0;
|
|
1065
|
+
});
|
|
1066
|
+
return list;
|
|
1067
|
+
}
|
|
1068
|
+
function parseSetDeps(deps) {
|
|
1069
|
+
return Object.keys(deps).reduce((acc, item) => {
|
|
1070
|
+
acc[item] = Array.from(deps[item]);
|
|
1071
|
+
sortStringList(acc[item]);
|
|
1072
|
+
return acc;
|
|
1073
|
+
}, {});
|
|
1074
|
+
}
|
|
1075
|
+
function getRelativePath(filePath) {
|
|
1076
|
+
return path.relative(process.cwd(), path.resolve(filePath));
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* 图标项目 CDN 地址
|
|
1081
|
+
*/
|
|
1082
|
+
const ICON_PROJECT_CDN = 'https://image-1251917893.cos.ap-guangzhou.myqcloud.com/pmd-icon/icon-project.json';
|
|
1083
|
+
/**
|
|
1084
|
+
* COS 基础配置
|
|
1085
|
+
*/
|
|
1086
|
+
const COS_BASE = {
|
|
1087
|
+
bucket: 'image-1251917893',
|
|
1088
|
+
region: 'ap-guangzhou',
|
|
1089
|
+
};
|
|
1090
|
+
/**
|
|
1091
|
+
* 图标项目流水线配置
|
|
1092
|
+
*/
|
|
1093
|
+
const ICON_PROJECT_PIPELINE_CONFIG = {
|
|
1094
|
+
mainRemotePipelineId: 'ea55dae018e14f3ab7cac84a76858621',
|
|
1095
|
+
mainPipelineId: 'p-c988624124334c24b6872b10053c763e',
|
|
1096
|
+
createDirRemotePipelineId: '1209c293e0024a6cb9fe6e5e188239cf',
|
|
1097
|
+
};
|
|
1098
|
+
/**
|
|
1099
|
+
* 图标项目 Webhook URL
|
|
1100
|
+
*/
|
|
1101
|
+
const ICON_PROJECT_WEBHOOK_URL = '331402f8-c274-4b99-a302-4404759ca25a';
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* 通过原始 API 启动蓝盾流水线
|
|
1105
|
+
* @param params - 启动参数
|
|
1106
|
+
* @returns 流水线执行结果
|
|
1107
|
+
*/
|
|
1108
|
+
async function startPipelineByRawApi({ data, pipelineId, userName, projectId: pId = 'tip-h5', }) {
|
|
1109
|
+
const result = await axios({
|
|
1110
|
+
method: 'POST',
|
|
1111
|
+
url: `https://devops.aow.com/ms/process/api/external/pipelines/${pipelineId}/build`,
|
|
1112
|
+
headers: {
|
|
1113
|
+
'X-DEVOPS-PROJECT-ID': pId,
|
|
1114
|
+
'X-DEVOPS-UID': userName || 'novlan1',
|
|
1115
|
+
},
|
|
1116
|
+
data,
|
|
1117
|
+
});
|
|
1118
|
+
return result.data;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* 获取所有图标项目配置
|
|
1123
|
+
* @returns 包含图标项目数据的 Promise
|
|
1124
|
+
*/
|
|
1125
|
+
function genIconProjects() {
|
|
1126
|
+
return axios.get(`${ICON_PROJECT_CDN}?v=${Date.now()}`);
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* 创建或更新图标项目
|
|
1130
|
+
* @param options - 创建/更新选项
|
|
1131
|
+
* @returns 操作结果
|
|
1132
|
+
*/
|
|
1133
|
+
async function createIconProjectOrUpdate({ projectNameZN = '', projectName = '', figmaFileId = '', figmaNodeId = '', iconfontPrefix = '', iconfontFamily = '', cdn, cos, staffName = '', isCreate, tags, }) {
|
|
1134
|
+
const { data } = await genIconProjects();
|
|
1135
|
+
if (data[projectName] && isCreate) {
|
|
1136
|
+
return {
|
|
1137
|
+
error: 'project already exists',
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
const createInfo = isCreate || !data[projectName]?.creator ? {
|
|
1141
|
+
creator: staffName,
|
|
1142
|
+
createTime: Date.now(),
|
|
1143
|
+
} : {};
|
|
1144
|
+
data[projectName] = {
|
|
1145
|
+
...data[projectName],
|
|
1146
|
+
projectNameZN,
|
|
1147
|
+
projectName,
|
|
1148
|
+
figmaFileId,
|
|
1149
|
+
figmaNodeId,
|
|
1150
|
+
iconfontPrefix,
|
|
1151
|
+
iconfontFamily,
|
|
1152
|
+
cdn: cdn || '',
|
|
1153
|
+
tags,
|
|
1154
|
+
...createInfo,
|
|
1155
|
+
updateTime: Date.now(),
|
|
1156
|
+
};
|
|
1157
|
+
const key = ICON_PROJECT_CDN.replace(/^https:\/\/[^/]+\//, '');
|
|
1158
|
+
console.log('[createIconProjectOrUpdate] data: ', { data, key });
|
|
1159
|
+
// if (isCreate) {
|
|
1160
|
+
await startPipelineByRawApi({
|
|
1161
|
+
data: {
|
|
1162
|
+
creator: staffName,
|
|
1163
|
+
projectName,
|
|
1164
|
+
toCreateDir: projectName,
|
|
1165
|
+
},
|
|
1166
|
+
pipelineId: ICON_PROJECT_PIPELINE_CONFIG.createDirRemotePipelineId,
|
|
1167
|
+
userName: staffName,
|
|
1168
|
+
});
|
|
1169
|
+
// }
|
|
1170
|
+
try {
|
|
1171
|
+
await uploadCOSStreamFile({
|
|
1172
|
+
secretId: cos.secretId,
|
|
1173
|
+
secretKey: cos.secretKey,
|
|
1174
|
+
bucket: cos.bucket,
|
|
1175
|
+
region: cos.region,
|
|
1176
|
+
key,
|
|
1177
|
+
file: {
|
|
1178
|
+
buffer: Buffer.from(JSON.stringify(data, null, 2)),
|
|
1179
|
+
},
|
|
1180
|
+
});
|
|
1181
|
+
await purgeUrlCache({
|
|
1182
|
+
secretId: cos.secretId,
|
|
1183
|
+
secretKey: cos.secretKey,
|
|
1184
|
+
urls: [toCdnUrl(ICON_PROJECT_CDN)],
|
|
1185
|
+
})
|
|
1186
|
+
.then((res) => {
|
|
1187
|
+
console.log('[purgeUrlCache] res: ', res?.data);
|
|
1188
|
+
})
|
|
1189
|
+
.catch((err) => {
|
|
1190
|
+
console.log('[purgeUrlCache] err: ', err);
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
catch (e) {
|
|
1194
|
+
return {
|
|
1195
|
+
error: e.message,
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
return {
|
|
1199
|
+
isCreate,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const BASE_SCSS = 'base.scss';
|
|
1204
|
+
function getStyleList(dir) {
|
|
1205
|
+
const cssList = fs.readdirSync(dir);
|
|
1206
|
+
const filtered = cssList
|
|
1207
|
+
.filter(item => item.endsWith('scss') && !item.startsWith(BASE_SCSS))
|
|
1208
|
+
.map(item => item.replace(/\.scss$/, ''));
|
|
1209
|
+
return filtered;
|
|
1210
|
+
}
|
|
1211
|
+
function genInjectContent({ styleList, componentName, topElement, dir = '', }) {
|
|
1212
|
+
const styleStr = styleList.map((item) => `
|
|
1213
|
+
&--type-${item} {
|
|
1214
|
+
@import './${dir}${item}.scss';
|
|
1215
|
+
}`).join('\n');
|
|
1216
|
+
return `
|
|
1217
|
+
${topElement}.${componentName} {
|
|
1218
|
+
${styleStr}
|
|
1219
|
+
}
|
|
1220
|
+
`;
|
|
1221
|
+
}
|
|
1222
|
+
function getComponentName(dir) {
|
|
1223
|
+
const tPath = normalizePath$1(dir);
|
|
1224
|
+
const reg = /\/([^/]+)\/css/;
|
|
1225
|
+
const match = tPath.match(reg);
|
|
1226
|
+
return match?.[1] || '';
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* 将 COS 文件同步到 Git 仓库
|
|
1231
|
+
* @param options - 同步选项
|
|
1232
|
+
*/
|
|
1233
|
+
async function cosSyncToGit({ staffName, }) {
|
|
1234
|
+
await startPipelineByRawApi({
|
|
1235
|
+
data: {
|
|
1236
|
+
creator: staffName || 'novlan1',
|
|
1237
|
+
},
|
|
1238
|
+
pipelineId: COS_TO_GIT_REMOTE_PIPELINE_ID,
|
|
1239
|
+
userName: staffName || 'novlan1',
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const LOADER_PROD = 'loader.prod.js';
|
|
1244
|
+
function getLoaderFile(dir = '', isProd = false) {
|
|
1245
|
+
if (isProd) {
|
|
1246
|
+
return path$1.resolve(dir, LOADER_PROD);
|
|
1247
|
+
}
|
|
1248
|
+
return path$1.resolve(dir, 'loader.js');
|
|
1249
|
+
}
|
|
1250
|
+
function getLoaderProdFile(dir = '') {
|
|
1251
|
+
return path$1.resolve(dir, LOADER_PROD);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const LOG_KEY = 'LOADER_LOGS';
|
|
1255
|
+
function saveLoaderLog() {
|
|
1256
|
+
const loaderLogs = global[LOG_KEY];
|
|
1257
|
+
if (!loaderLogs)
|
|
1258
|
+
return;
|
|
1259
|
+
Object.keys(loaderLogs).forEach((file) => {
|
|
1260
|
+
saveJsonToLog(loaderLogs[file], file);
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
function recordLoaderLog(file, content) {
|
|
1264
|
+
if (!global[LOG_KEY]) {
|
|
1265
|
+
global[LOG_KEY] = {};
|
|
1266
|
+
}
|
|
1267
|
+
if (!global[LOG_KEY][file]) {
|
|
1268
|
+
global[LOG_KEY][file] = [];
|
|
1269
|
+
}
|
|
1270
|
+
global[LOG_KEY][file].push(content);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function shouldUseLoader(defaultPlatforms = []) {
|
|
1274
|
+
const options = getOptions(this) || {};
|
|
1275
|
+
const { platforms = defaultPlatforms } = options;
|
|
1276
|
+
const platform = process.env.UNI_PLATFORM || '';
|
|
1277
|
+
if (platforms === ALL_PLATFORM
|
|
1278
|
+
|| platforms.indexOf(ALL_PLATFORM) > -1) {
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
return platforms.includes(platform);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* 研发平台 MCP 公共认证模块
|
|
1286
|
+
*/
|
|
1287
|
+
/**
|
|
1288
|
+
* 解析命令行参数
|
|
1289
|
+
* @param defaultApiUrl 默认的 API URL
|
|
1290
|
+
* @returns 包含 rid, rtoken, apiUrl 和所有其他参数的对象
|
|
1291
|
+
*/
|
|
1292
|
+
function parseMCPCommandLineArgs(defaultApiUrl) {
|
|
1293
|
+
const args = process.argv.slice(2).reduce((acc, arg) => {
|
|
1294
|
+
const parts = arg.split('=');
|
|
1295
|
+
// 只处理格式正确的参数(包含等号且有值)
|
|
1296
|
+
if (parts.length >= 2) {
|
|
1297
|
+
const key = parts[0].replace('--', '');
|
|
1298
|
+
const value = parts.slice(1).join('='); // 处理值中包含等号的情况
|
|
1299
|
+
if (key && value) {
|
|
1300
|
+
acc[key] = value;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return acc;
|
|
1304
|
+
}, {});
|
|
1305
|
+
const rid = args.rid || '';
|
|
1306
|
+
const rtoken = args.rtoken || '';
|
|
1307
|
+
const apiUrl = args.apiUrl || defaultApiUrl;
|
|
1308
|
+
if (!rid) {
|
|
1309
|
+
console.error('错误:缺少必需的参数 --rid');
|
|
1310
|
+
}
|
|
1311
|
+
if (!rtoken) {
|
|
1312
|
+
console.error('错误:缺少必需的参数 --rtoken');
|
|
1313
|
+
}
|
|
1314
|
+
// 返回所有参数,但确保 rid、rtoken、apiUrl 使用处理后的值
|
|
1315
|
+
return { ...args, rid, rtoken, apiUrl };
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* 验证凭证
|
|
1319
|
+
*/
|
|
1320
|
+
function validateMCPCredentials(rid, rtoken) {
|
|
1321
|
+
if (!rid || !rtoken) {
|
|
1322
|
+
return {
|
|
1323
|
+
content: [
|
|
1324
|
+
{
|
|
1325
|
+
type: 'text',
|
|
1326
|
+
text: '❌ 错误:缺少必需的凭证参数\n\n请确保启动 MCP 服务时提供了 --rid 和 --rtoken 参数(https://mobile.aow.com/rd-platform-web/#/management/token)。\n\n示例:\nnode dist/index.js --rid=YOUR_RID --rtoken=YOUR_RTOKEN',
|
|
1327
|
+
},
|
|
1328
|
+
],
|
|
1329
|
+
isError: true,
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
return null;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* 获取 MCP 名称(去掉 前缀)
|
|
1337
|
+
* @param pkg package.json 对象
|
|
1338
|
+
* @returns MCP 名称
|
|
1339
|
+
* @example
|
|
1340
|
+
* import pkg from '../package.json';
|
|
1341
|
+
* const mcpName = getMcpName(pkg);
|
|
1342
|
+
*/
|
|
1343
|
+
function getMcpName(pkg) {
|
|
1344
|
+
if (!pkg.name) {
|
|
1345
|
+
throw new Error('package.json 中未找到 name 字段');
|
|
1346
|
+
}
|
|
1347
|
+
// 去掉 前缀
|
|
1348
|
+
return pkg.name.replace(/^@tencent\//, '');
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* 获取 MCP 版本
|
|
1352
|
+
* @param pkg package.json 对象
|
|
1353
|
+
* @returns 版本号
|
|
1354
|
+
* @example
|
|
1355
|
+
* import pkg from '../package.json';
|
|
1356
|
+
* const mcpVersion = getMcpVersion(pkg);
|
|
1357
|
+
*/
|
|
1358
|
+
function getMcpVersion(pkg) {
|
|
1359
|
+
if (!pkg.version) {
|
|
1360
|
+
throw new Error('package.json 中未找到 version 字段');
|
|
1361
|
+
}
|
|
1362
|
+
return pkg.version;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* 研发平台 MCP 公共 HTTP 模块
|
|
1367
|
+
*/
|
|
1368
|
+
/**
|
|
1369
|
+
* 通用错误处理
|
|
1370
|
+
*/
|
|
1371
|
+
function handleAxiosError(error, actionName) {
|
|
1372
|
+
console.error(`❌ ${actionName}失败\n`);
|
|
1373
|
+
// 处理网络错误
|
|
1374
|
+
if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
|
1375
|
+
console.error(` 网络错误: ${error.message}\n`);
|
|
1376
|
+
return { success: false, message: '网络连接失败,无法访问研发平台服务' };
|
|
1377
|
+
}
|
|
1378
|
+
// 处理超时错误
|
|
1379
|
+
if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') {
|
|
1380
|
+
console.error(` 请求超时: ${error.message}\n`);
|
|
1381
|
+
return { success: false, message: '请求超时,请检查网络连接后重试' };
|
|
1382
|
+
}
|
|
1383
|
+
// 处理 HTTP 错误响应
|
|
1384
|
+
if (error.response) {
|
|
1385
|
+
const { status } = error.response;
|
|
1386
|
+
const { data } = error.response;
|
|
1387
|
+
console.error(` HTTP 错误: ${status}\n`);
|
|
1388
|
+
console.error(` 响应详情: ${JSON.stringify(data, null, 2)}\n`);
|
|
1389
|
+
if (status === 401 || status === 403) {
|
|
1390
|
+
return { success: false, message: '认证失败,rid 或 rtoken 无效或已过期' };
|
|
1391
|
+
}
|
|
1392
|
+
return {
|
|
1393
|
+
success: false,
|
|
1394
|
+
message: `服务器返回错误 (${status}): ${data?.msg || data?.message || '未知错误'}`,
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
// 其他未知错误
|
|
1398
|
+
console.error(` 未知错误: ${error.message}\n`);
|
|
1399
|
+
return { success: false, message: `调用${actionName}时发生未知错误: ${error.message}` };
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* 调用研发平台 API
|
|
1403
|
+
* @param apiUrl API 地址
|
|
1404
|
+
* @param requestBody 请求体
|
|
1405
|
+
* @param rid 用户 rid
|
|
1406
|
+
* @param rtoken 用户 token
|
|
1407
|
+
* @param pkg package.json 对象(用于获取 MCP 名称和版本)
|
|
1408
|
+
* @example
|
|
1409
|
+
* import pkg from '../package.json';
|
|
1410
|
+
* await callRdApi(apiUrl, requestBody, rid, rtoken, pkg);
|
|
1411
|
+
*/
|
|
1412
|
+
async function callRdMCPApi(apiUrl, requestBody, rid, rtoken, pkg) {
|
|
1413
|
+
// 获取 MCP 名称和版本
|
|
1414
|
+
const mcpName = getMcpName(pkg);
|
|
1415
|
+
const mcpVersion = getMcpVersion(pkg);
|
|
1416
|
+
const logPrefix = `[${mcpName}]`;
|
|
1417
|
+
console.log(`${logPrefix} ⏳ 正在调用接口...`);
|
|
1418
|
+
console.log(`${logPrefix} API: ${apiUrl}`);
|
|
1419
|
+
console.log(`${logPrefix} 请求参数: ${JSON.stringify(requestBody, null, 2)}`);
|
|
1420
|
+
console.log('');
|
|
1421
|
+
try {
|
|
1422
|
+
const response = await axios.post(apiUrl, {
|
|
1423
|
+
mcpName,
|
|
1424
|
+
mcpVersion,
|
|
1425
|
+
...requestBody,
|
|
1426
|
+
}, {
|
|
1427
|
+
headers: {
|
|
1428
|
+
'Content-Type': 'application/json',
|
|
1429
|
+
'User-Agent': mcpName,
|
|
1430
|
+
rid,
|
|
1431
|
+
rtoken,
|
|
1432
|
+
},
|
|
1433
|
+
});
|
|
1434
|
+
// 检查业务状态码(兼容 code 和 r 两种字段)
|
|
1435
|
+
const statusCode = response.data.code ?? response.data.r ?? 0;
|
|
1436
|
+
const errMsg = response.data.msg || response.data.message || '未知错误';
|
|
1437
|
+
if (statusCode < 0) {
|
|
1438
|
+
console.log(`${logPrefix} ❌ 接口调用失败: ${errMsg}\n`);
|
|
1439
|
+
console.log(`${logPrefix} 响应详情: ${JSON.stringify(response.data, null, 2)}\n`);
|
|
1440
|
+
const errorMessage = `${errMsg}\n\n请求参数:\n${JSON.stringify(requestBody, null, 2)}`;
|
|
1441
|
+
return { success: false, message: errorMessage, code: statusCode };
|
|
1442
|
+
}
|
|
1443
|
+
console.log(`${logPrefix} ✅ 接口调用成功\n`);
|
|
1444
|
+
console.log(`${logPrefix} 响应详情: ${JSON.stringify(response.data, null, 2)}\n`);
|
|
1445
|
+
return { success: true, data: response.data.data };
|
|
1446
|
+
}
|
|
1447
|
+
catch (error) {
|
|
1448
|
+
return handleAxiosError(error, `${logPrefix} 接口`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* 小程序 CI 模板 ID 映射
|
|
1454
|
+
*/
|
|
1455
|
+
const TEMPLATE_ID_MAP = {
|
|
1456
|
+
/** 微信小程序 CI 模板 ID */
|
|
1457
|
+
WX_MP_CI: '864ecf9343fb4748adfdecde92712401',
|
|
1458
|
+
/** QQ 小程序 CI 模板 ID */
|
|
1459
|
+
QQ_MP_CI: '6c13205d64fe4a42889db131b5e47d03',
|
|
1460
|
+
/** Cypress 自动化测试模板 ID */
|
|
1461
|
+
CYPRESS_AUTO_TEST: 'f7b13cf8929343a1bd56949e5463dfc4',
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
1465
|
+
/**
|
|
1466
|
+
* Rainbow 配置常量
|
|
1467
|
+
*/
|
|
1468
|
+
const RAINBOW_CONFIG$1 = {
|
|
1469
|
+
valueType: 4,
|
|
1470
|
+
creator: 'novlan1',
|
|
1471
|
+
approvers: 'novlan1;Rainbow_ygw',
|
|
1472
|
+
envName: 'Default',
|
|
1473
|
+
groupName: 'devops_mp_ci',
|
|
1474
|
+
};
|
|
1475
|
+
/**
|
|
1476
|
+
* 获取小程序 CI 通用配置
|
|
1477
|
+
* @returns 通用配置对象
|
|
1478
|
+
*/
|
|
1479
|
+
async function getMpCICommonConfig() {
|
|
1480
|
+
const { RAINBOW_APP_ID, } = process.env;
|
|
1481
|
+
const { envName, groupName, } = RAINBOW_CONFIG$1;
|
|
1482
|
+
let res = await fetchRainbowConfigFromSdk({
|
|
1483
|
+
secretInfo: {
|
|
1484
|
+
appId: RAINBOW_APP_ID,
|
|
1485
|
+
userId: process.env.RAINBOW_USER_ID,
|
|
1486
|
+
secretKey: process.env.RAINBOW_SECRET_KEY || '',
|
|
1487
|
+
envName,
|
|
1488
|
+
groupName,
|
|
1489
|
+
},
|
|
1490
|
+
key: 'common_config',
|
|
1491
|
+
sdk: require('@tencent/rainbow-node-sdk'),
|
|
1492
|
+
rainbow: global.rainbowInstance,
|
|
1493
|
+
tryJsonParse: false,
|
|
1494
|
+
});
|
|
1495
|
+
try {
|
|
1496
|
+
res = JSON.parse(res);
|
|
1497
|
+
}
|
|
1498
|
+
catch (err) { }
|
|
1499
|
+
return res;
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* 获取 Rainbow 配置
|
|
1503
|
+
* @param params - 查询参数
|
|
1504
|
+
* @returns Rainbow 配置项列表
|
|
1505
|
+
*/
|
|
1506
|
+
async function getRainbowConfig({ rdProjectId, projectName, subProject, }) {
|
|
1507
|
+
const { RAINBOW_APP_ID, RAINBOW_USER_ID, RAINBOW_SECRET_KEY, } = process.env;
|
|
1508
|
+
const resp = await queryGroupInfo({
|
|
1509
|
+
secretInfo: {
|
|
1510
|
+
appId: RAINBOW_APP_ID,
|
|
1511
|
+
envName: 'Default',
|
|
1512
|
+
groupName: RAINBOW_CONFIG$1.groupName,
|
|
1513
|
+
userId: RAINBOW_USER_ID,
|
|
1514
|
+
secretKey: RAINBOW_SECRET_KEY || '',
|
|
1515
|
+
},
|
|
1516
|
+
});
|
|
1517
|
+
const result = resp
|
|
1518
|
+
.filter((item) => item.key.endsWith('_mp_ci'))
|
|
1519
|
+
.map((item) => {
|
|
1520
|
+
let data = {};
|
|
1521
|
+
try {
|
|
1522
|
+
data = JSON.parse(item.value);
|
|
1523
|
+
}
|
|
1524
|
+
catch (err) { }
|
|
1525
|
+
return {
|
|
1526
|
+
rainbowKey: item.key,
|
|
1527
|
+
...data,
|
|
1528
|
+
};
|
|
1529
|
+
})
|
|
1530
|
+
.filter((item) => ((rdProjectId && item.rdProjectId === rdProjectId)
|
|
1531
|
+
|| (projectName && item.ci?.repo === projectName))
|
|
1532
|
+
&& item.subProject === subProject);
|
|
1533
|
+
return result;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* 更新小程序 CI 配置
|
|
1537
|
+
* @param value - 配置值
|
|
1538
|
+
* @param key - 配置 Key
|
|
1539
|
+
*/
|
|
1540
|
+
async function updateMpCiConfig(value, key) {
|
|
1541
|
+
const { RAINBOW_APP_ID, RAINBOW_USER_ID, RAINBOW_SECRET_KEY, } = process.env;
|
|
1542
|
+
const { valueType, creator, approvers, envName, groupName, } = RAINBOW_CONFIG$1;
|
|
1543
|
+
await updateRainbowKVAndPublish({
|
|
1544
|
+
key,
|
|
1545
|
+
value: JSON.stringify(value, null, 2),
|
|
1546
|
+
valueType,
|
|
1547
|
+
creator,
|
|
1548
|
+
approvers,
|
|
1549
|
+
secretInfo: {
|
|
1550
|
+
appId: RAINBOW_APP_ID,
|
|
1551
|
+
userId: RAINBOW_USER_ID,
|
|
1552
|
+
secretKey: RAINBOW_SECRET_KEY || '',
|
|
1553
|
+
envName,
|
|
1554
|
+
groupName,
|
|
1555
|
+
},
|
|
1556
|
+
}).then((res) => {
|
|
1557
|
+
console.log('[updateMpCiConfig] res', res);
|
|
1558
|
+
})
|
|
1559
|
+
.catch((err) => {
|
|
1560
|
+
console.log('[updateMpCiConfig] err', err);
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* 获取所有小程序流水线实例
|
|
1565
|
+
* @returns 微信和 QQ 小程序流水线实例
|
|
1566
|
+
*/
|
|
1567
|
+
async function getAllMpPipeline() {
|
|
1568
|
+
const param = {
|
|
1569
|
+
projectId: 'tip-h5',
|
|
1570
|
+
host: 'http://devops.apigw.o.aow.com',
|
|
1571
|
+
secretInfo: {
|
|
1572
|
+
appCode: 'tip-tool',
|
|
1573
|
+
appSecret: process.env.DEVOPS_APP_SECRET || '',
|
|
1574
|
+
devopsUid: 'novlan1',
|
|
1575
|
+
},
|
|
1576
|
+
};
|
|
1577
|
+
const wxInstances = await getAllDevopsTemplateInstances({
|
|
1578
|
+
...param,
|
|
1579
|
+
templateId: TEMPLATE_ID_MAP.WX_MP_CI,
|
|
1580
|
+
});
|
|
1581
|
+
const qqInstances = await getAllDevopsTemplateInstances({
|
|
1582
|
+
...param,
|
|
1583
|
+
templateId: TEMPLATE_ID_MAP.QQ_MP_CI,
|
|
1584
|
+
});
|
|
1585
|
+
return {
|
|
1586
|
+
wxInstances,
|
|
1587
|
+
qqInstances,
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* 获取指定 Key 的小程序 CI 配置
|
|
1592
|
+
* @param key - 配置 Key
|
|
1593
|
+
* @returns 配置值
|
|
1594
|
+
*/
|
|
1595
|
+
async function getMpCIKeyConfig(key) {
|
|
1596
|
+
const { RAINBOW_APP_ID, } = process.env;
|
|
1597
|
+
const { envName, groupName, } = RAINBOW_CONFIG$1;
|
|
1598
|
+
let res = await fetchRainbowConfigFromSdk({
|
|
1599
|
+
secretInfo: {
|
|
1600
|
+
appId: RAINBOW_APP_ID,
|
|
1601
|
+
userId: process.env.RAINBOW_USER_ID,
|
|
1602
|
+
secretKey: process.env.RAINBOW_SECRET_KEY || '',
|
|
1603
|
+
envName,
|
|
1604
|
+
groupName,
|
|
1605
|
+
},
|
|
1606
|
+
key,
|
|
1607
|
+
sdk: require('@tencent/rainbow-node-sdk'),
|
|
1608
|
+
rainbow: global.rainbowInstanceNoCache,
|
|
1609
|
+
tryJsonParse: false,
|
|
1610
|
+
});
|
|
1611
|
+
try {
|
|
1612
|
+
res = JSON.parse(res);
|
|
1613
|
+
}
|
|
1614
|
+
catch (err) { }
|
|
1615
|
+
return res;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* 从 repo 地址中提取仓库路径
|
|
1620
|
+
* 支持 git@git.aow.com:pmd-mobile/pmd-h5/plugin-light.git 和 https://github.com/novlan1/plugin-light 两种格式
|
|
1621
|
+
* @param repo 仓库地址
|
|
1622
|
+
* @returns 仓库路径,如 pmd-mobile/pmd-h5/plugin-light
|
|
1623
|
+
*/
|
|
1624
|
+
function extractRepoPath(repo) {
|
|
1625
|
+
if (!repo)
|
|
1626
|
+
return '';
|
|
1627
|
+
// 去除末尾的 .git
|
|
1628
|
+
const cleanRepo = repo.replace(/\.git$/, '');
|
|
1629
|
+
// SSH 格式: git@git.aow.com:pmd-mobile/pmd-h5/plugin-light
|
|
1630
|
+
if (cleanRepo.includes('@') && cleanRepo.includes(':')) {
|
|
1631
|
+
const match = cleanRepo.match(/:(.+)$/);
|
|
1632
|
+
return match ? match[1] : '';
|
|
1633
|
+
}
|
|
1634
|
+
// HTTPS 格式: https://github.com/novlan1/plugin-light
|
|
1635
|
+
if (cleanRepo.includes('://')) {
|
|
1636
|
+
const match = cleanRepo.match(/git\.aow\.com\/(.+)$/);
|
|
1637
|
+
return match ? match[1] : '';
|
|
1638
|
+
}
|
|
1639
|
+
// 已经是路径格式
|
|
1640
|
+
return cleanRepo;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* 根据分支获取环境
|
|
1645
|
+
* @param branch - 分支名称
|
|
1646
|
+
* @returns 环境名称,release 分支返回 'release',其他返回 'test'
|
|
1647
|
+
*/
|
|
1648
|
+
function getEnvByBranch(branch) {
|
|
1649
|
+
if (branch === 'release') {
|
|
1650
|
+
return 'release';
|
|
1651
|
+
}
|
|
1652
|
+
return 'test';
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* 解析发布原因,替换特殊字符
|
|
1656
|
+
* @param str - 原始发布原因字符串
|
|
1657
|
+
* @returns 处理后的发布原因字符串
|
|
1658
|
+
*/
|
|
1659
|
+
function parsePublishReason(str = '') {
|
|
1660
|
+
return str.replace(/[`'"]/g, '·').replace(/\n/g, ';');
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* 添加小程序发布记录
|
|
1665
|
+
* @param params - 记录参数
|
|
1666
|
+
*/
|
|
1667
|
+
async function addMPPublishRecord({ staffName, repo, subProjectName, branch, env, isWeixin, pipelineId, pipelineRunId, publishReason, fromMcp, mcpName, version, operationTool, mcpDB, }) {
|
|
1668
|
+
const recordList = [
|
|
1669
|
+
isWeixin ? '微信小程序' : 'QQ小程序',
|
|
1670
|
+
`工程:${repo}`,
|
|
1671
|
+
`子工程:${subProjectName}`,
|
|
1672
|
+
`分支:${branch}`,
|
|
1673
|
+
`环境:${env}`,
|
|
1674
|
+
publishReason ? `发布原因:${publishReason}` : '',
|
|
1675
|
+
].filter(Boolean);
|
|
1676
|
+
const desc = [
|
|
1677
|
+
...recordList,
|
|
1678
|
+
`MCP:${fromMcp ? '是' : '否'}`,
|
|
1679
|
+
].filter(Boolean).join(',');
|
|
1680
|
+
const list = repo.split('/');
|
|
1681
|
+
const groupName = list.slice(0, list.length - 1).join('/');
|
|
1682
|
+
const operationData = [
|
|
1683
|
+
// req.header('staffname') || creator || 'novlan1',
|
|
1684
|
+
staffName,
|
|
1685
|
+
// operationTool.TYPE.MP_BUILD,
|
|
1686
|
+
'小程序构建',
|
|
1687
|
+
desc,
|
|
1688
|
+
// operationTool.STATUS.START,
|
|
1689
|
+
'已启动',
|
|
1690
|
+
pipelineId,
|
|
1691
|
+
pipelineRunId,
|
|
1692
|
+
'',
|
|
1693
|
+
'',
|
|
1694
|
+
groupName,
|
|
1695
|
+
repo,
|
|
1696
|
+
subProjectName,
|
|
1697
|
+
];
|
|
1698
|
+
if (fromMcp && typeof mcpDB?.add === 'function') {
|
|
1699
|
+
const mcpContent = recordList.join(',');
|
|
1700
|
+
await mcpDB.add(mcpName || 'mp-publish-mcp', staffName, mcpContent, version);
|
|
1701
|
+
}
|
|
1702
|
+
operationTool.add(...operationData);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* 启动小程序 CI 流水线
|
|
1707
|
+
* @param params - 启动参数
|
|
1708
|
+
* @returns 执行结果
|
|
1709
|
+
*/
|
|
1710
|
+
async function startMpCIPipeline({ repo: rawRepo, subProject, branch, staffName, isWeixin, publishReason = '', mcpName, mcpVersion, startPipeline, operationTool, mcpDB, }) {
|
|
1711
|
+
const env = getEnvByBranch(branch);
|
|
1712
|
+
const repo = extractRepoPath(rawRepo);
|
|
1713
|
+
const rainbowConfigs = await getRainbowConfig({
|
|
1714
|
+
projectName: repo,
|
|
1715
|
+
subProject,
|
|
1716
|
+
});
|
|
1717
|
+
if (!rainbowConfigs?.length) {
|
|
1718
|
+
return {
|
|
1719
|
+
r: -1,
|
|
1720
|
+
msg: '未找到对应的七彩石配置',
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
const rainbowConfig = rainbowConfigs[0];
|
|
1724
|
+
const robotMap = isWeixin
|
|
1725
|
+
? rainbowConfig.robotMap
|
|
1726
|
+
: rainbowConfig.qqRobotMap;
|
|
1727
|
+
const curRobot = robotMap?.[branch]?.[env];
|
|
1728
|
+
if (!curRobot) {
|
|
1729
|
+
// TODO: 新增机器人配置
|
|
1730
|
+
return {
|
|
1731
|
+
r: -1,
|
|
1732
|
+
msg: '未找到对应的机器人配置',
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
const { wxInstances, qqInstances, } = await getAllMpPipeline();
|
|
1736
|
+
const instances = isWeixin ? wxInstances : qqInstances;
|
|
1737
|
+
const projectCiName = rainbowConfig?.ci?.name;
|
|
1738
|
+
const prefix = isWeixin ? 'wxci' : 'qqci';
|
|
1739
|
+
const foundInstance = instances.find((item) => item.pipelineName.startsWith(`${prefix}__${projectCiName}__${curRobot}__`));
|
|
1740
|
+
if (!foundInstance) {
|
|
1741
|
+
return {
|
|
1742
|
+
r: -1,
|
|
1743
|
+
msg: '未找到对应的流水线',
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
const { pipelineId } = foundInstance;
|
|
1747
|
+
const parsedPublishReason = parsePublishReason(publishReason || '');
|
|
1748
|
+
const result = await startPipeline(pipelineId, {
|
|
1749
|
+
creator: staffName,
|
|
1750
|
+
rainbowConfigKey: rainbowConfig.rainbowKey,
|
|
1751
|
+
publishReason: parsedPublishReason,
|
|
1752
|
+
isWeixin: isWeixin ? 1 : 0,
|
|
1753
|
+
repo,
|
|
1754
|
+
subProjectName: subProject,
|
|
1755
|
+
branch,
|
|
1756
|
+
env,
|
|
1757
|
+
useDevopsWXCIPlugin: rainbowConfig?.devopsParams?.useDevopsWXCIPlugin ?? '1',
|
|
1758
|
+
});
|
|
1759
|
+
await addMPPublishRecord({
|
|
1760
|
+
staffName,
|
|
1761
|
+
repo,
|
|
1762
|
+
subProjectName: subProject,
|
|
1763
|
+
branch,
|
|
1764
|
+
env,
|
|
1765
|
+
isWeixin,
|
|
1766
|
+
pipelineId,
|
|
1767
|
+
pipelineRunId: result.data?.id,
|
|
1768
|
+
publishReason: parsedPublishReason,
|
|
1769
|
+
fromMcp: true,
|
|
1770
|
+
mcpName,
|
|
1771
|
+
version: mcpVersion,
|
|
1772
|
+
operationTool,
|
|
1773
|
+
mcpDB,
|
|
1774
|
+
});
|
|
1775
|
+
return {
|
|
1776
|
+
r: result.status ?? 0,
|
|
1777
|
+
msg: result.message ?? result.data?.id,
|
|
1778
|
+
result: result.data,
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function findNodeModuleFile({ name, target, filePath, root, }) {
|
|
1783
|
+
const iRoot = root ?? process.cwd();
|
|
1784
|
+
const NODE_MODULES = 'node_modules';
|
|
1785
|
+
const PNPM = `${NODE_MODULES}/.pnpm`;
|
|
1786
|
+
const nodeModulesTargetFile = path$1.resolve(iRoot, NODE_MODULES, name, filePath);
|
|
1787
|
+
const exist = fs$1.existsSync(nodeModulesTargetFile);
|
|
1788
|
+
if (exist) {
|
|
1789
|
+
return [
|
|
1790
|
+
nodeModulesTargetFile,
|
|
1791
|
+
];
|
|
1792
|
+
}
|
|
1793
|
+
const pnpmRoot = path$1.resolve(iRoot, PNPM);
|
|
1794
|
+
if (!fs$1.existsSync(pnpmRoot)) {
|
|
1795
|
+
return [];
|
|
1796
|
+
}
|
|
1797
|
+
const pnpmList = fs$1.readdirSync(pnpmRoot);
|
|
1798
|
+
const list = pnpmList.filter(item => item.startsWith(target));
|
|
1799
|
+
const innerFileList = list.map((file) => {
|
|
1800
|
+
const targetFile = path$1.resolve(`${PNPM}/${file}/node_modules/${name}/${filePath}`);
|
|
1801
|
+
return targetFile;
|
|
1802
|
+
}).filter(file => fs$1.existsSync(file));
|
|
1803
|
+
return innerFileList;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
function getPipelineRunLink({ pipelineId, pipelineRunId, }) {
|
|
1807
|
+
return `https://devops.aow.com/console/pipeline/tip-h5/${pipelineId}/detail/${pipelineRunId}/executeDetail`;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
/**
|
|
1811
|
+
* 启动 NPM 发布流水线核心函数
|
|
1812
|
+
* @param params - 启动参数
|
|
1813
|
+
* @returns 执行结果
|
|
1814
|
+
*/
|
|
1815
|
+
async function startNPMPublishPipelineCore({ form, staffName, remotePipelineId, componentTitle, mcpDB, fromMcp, mcpName, mcpVersion, operationTool, }) {
|
|
1816
|
+
const { branch, publishReason, versionType, publishExternalNPM, publishPackage, ...args } = form || {};
|
|
1817
|
+
if (!branch) {
|
|
1818
|
+
return { r: -1, msg: '缺少分支名称' };
|
|
1819
|
+
}
|
|
1820
|
+
if (!versionType) {
|
|
1821
|
+
return { r: -1, msg: '缺少发布版本类型' };
|
|
1822
|
+
}
|
|
1823
|
+
if (!publishReason) {
|
|
1824
|
+
return { r: -1, msg: '缺少发布原因' };
|
|
1825
|
+
}
|
|
1826
|
+
try {
|
|
1827
|
+
const result = await startPipelineByRawApi({
|
|
1828
|
+
data: {
|
|
1829
|
+
branch,
|
|
1830
|
+
versionType,
|
|
1831
|
+
publishReason,
|
|
1832
|
+
publishExternalNPM,
|
|
1833
|
+
creator: staffName,
|
|
1834
|
+
publishPackage,
|
|
1835
|
+
...args,
|
|
1836
|
+
},
|
|
1837
|
+
pipelineId: remotePipelineId,
|
|
1838
|
+
userName: staffName,
|
|
1839
|
+
});
|
|
1840
|
+
if (!result?.status) {
|
|
1841
|
+
const recordList = [
|
|
1842
|
+
`包名:${componentTitle}`,
|
|
1843
|
+
`分支:${branch}`,
|
|
1844
|
+
`版本类型:${versionType}`,
|
|
1845
|
+
`发布原因:${publishReason}`,
|
|
1846
|
+
`是否发布外网:${publishExternalNPM ? '是' : '否'}`,
|
|
1847
|
+
publishPackage ? `发布子包:${publishPackage}` : '',
|
|
1848
|
+
].filter(Boolean);
|
|
1849
|
+
await operationTool.addByMapParam({
|
|
1850
|
+
staffName,
|
|
1851
|
+
type: 'NPM_PUBLISH',
|
|
1852
|
+
desc: [
|
|
1853
|
+
...recordList,
|
|
1854
|
+
`MCP:${fromMcp ? '是' : '否'}`,
|
|
1855
|
+
mcpVersion ? `MCP版本:${mcpVersion}` : '',
|
|
1856
|
+
].filter(Boolean).join(','),
|
|
1857
|
+
status: 'success',
|
|
1858
|
+
extra: {
|
|
1859
|
+
pipelineId: result.data?.pipelineId,
|
|
1860
|
+
pipelineRunId: result.data?.id,
|
|
1861
|
+
},
|
|
1862
|
+
});
|
|
1863
|
+
if (typeof mcpDB?.add === 'function') {
|
|
1864
|
+
const mcpContent = recordList.join(',');
|
|
1865
|
+
await mcpDB.add(mcpName || 'npm-publish-mcp', staffName, mcpContent, mcpVersion);
|
|
1866
|
+
}
|
|
1867
|
+
return { r: 0, result };
|
|
1868
|
+
}
|
|
1869
|
+
return { r: -1, msg: result.message };
|
|
1870
|
+
}
|
|
1871
|
+
catch (err) {
|
|
1872
|
+
return { r: -1, msg: err.message };
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* 根据 repo 获取组件配置
|
|
1878
|
+
* @param repo 仓库地址,支持 SSH 和 HTTPS 格式
|
|
1879
|
+
* @returns 组件配置,未找到则返回 null
|
|
1880
|
+
*/
|
|
1881
|
+
function getComponentByRepo(repo) {
|
|
1882
|
+
if (!repo)
|
|
1883
|
+
return null;
|
|
1884
|
+
const repoPath = extractRepoPath(repo);
|
|
1885
|
+
if (!repoPath)
|
|
1886
|
+
return null;
|
|
1887
|
+
// 遍历二维数组查找匹配的组件
|
|
1888
|
+
for (const component of NPM_PACKAGES_LIST) {
|
|
1889
|
+
const componentRepoPath = extractRepoPath(component.git);
|
|
1890
|
+
if (componentRepoPath === repoPath) {
|
|
1891
|
+
return component;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
return null;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function getWxmlAndWxssPostfix() {
|
|
1898
|
+
const map = Object.keys(PLATFORM_MAP).reduce((acc, item) => {
|
|
1899
|
+
acc[PLATFORM_MAP[item]] = item;
|
|
1900
|
+
return acc;
|
|
1901
|
+
}, {});
|
|
1902
|
+
const key = map[process.env.UNI_PLATFORM || ''];
|
|
1903
|
+
return [
|
|
1904
|
+
HTML_MAP[key],
|
|
1905
|
+
CSS_MAP[key],
|
|
1906
|
+
];
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function getProjectName() {
|
|
1910
|
+
let result = '';
|
|
1911
|
+
try {
|
|
1912
|
+
const json = readFileSync('package.json', true) || {};
|
|
1913
|
+
result = json.name || '';
|
|
1914
|
+
}
|
|
1915
|
+
catch (err) { }
|
|
1916
|
+
return result;
|
|
1917
|
+
}
|
|
1918
|
+
function getSubProjectName() {
|
|
1919
|
+
const name = process.env.VUE_APP_DIR?.split('/')?.[1] || '';
|
|
1920
|
+
return name;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function updateManifestCore(_a) {
|
|
1924
|
+
var path = _a.path,
|
|
1925
|
+
value = _a.value,
|
|
1926
|
+
manifest = _a.manifest;
|
|
1927
|
+
var arr = path.split('.');
|
|
1928
|
+
var len = arr.length;
|
|
1929
|
+
var lastItem = arr[len - 1];
|
|
1930
|
+
var i = 0;
|
|
1931
|
+
var manifestArr = manifest.split(/\n/);
|
|
1932
|
+
for (var index = 0; index < manifestArr.length; index++) {
|
|
1933
|
+
var item = manifestArr[index];
|
|
1934
|
+
if (new RegExp("\"".concat(arr[i], "\"")).test(item)) i = i + 1;
|
|
1935
|
+
if (i === len) {
|
|
1936
|
+
var hasComma = /,/.test(item);
|
|
1937
|
+
manifestArr[index] = item.replace(new RegExp("\"".concat(lastItem, "\"[\\s\\S]*:[\\s\\S]*")), "\"".concat(lastItem, "\": ").concat(value).concat(hasComma ? ',' : ''));
|
|
1938
|
+
break;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
return manifestArr.join('\n');
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// 读取 manifest.json ,修改后重新写入
|
|
1945
|
+
const manifestPath = `${process.env.UNI_INPUT_DIR}/manifest.json`;
|
|
1946
|
+
let originManifest = '';
|
|
1947
|
+
try {
|
|
1948
|
+
originManifest = fs.readFileSync(manifestPath, { encoding: 'utf-8' });
|
|
1949
|
+
}
|
|
1950
|
+
catch (err) {
|
|
1951
|
+
}
|
|
1952
|
+
let Manifest = originManifest;
|
|
1953
|
+
function updateManifest(path, value) {
|
|
1954
|
+
Manifest = updateManifestCore({
|
|
1955
|
+
path,
|
|
1956
|
+
value,
|
|
1957
|
+
manifest: Manifest,
|
|
1958
|
+
});
|
|
1959
|
+
fs.writeFileSync(manifestPath, Manifest, {
|
|
1960
|
+
flag: 'w',
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
function revertManifest() {
|
|
1964
|
+
fs.writeFileSync(manifestPath, originManifest, {
|
|
1965
|
+
flag: 'w',
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function replaceDirective(source, list) {
|
|
1970
|
+
if (!list.length)
|
|
1971
|
+
return source;
|
|
1972
|
+
const reg = new RegExp(`(?<=<[^<]+)v-${list.join('|')}=?[^\\s]*`, 'g');
|
|
1973
|
+
const newSource = source.replace(reg, '');
|
|
1974
|
+
return newSource;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function getSubProjectRoot({ root, appDir, }) {
|
|
1978
|
+
let subProjectRoot = `${path$1.resolve(root, `./src/${appDir}`)}/`;
|
|
1979
|
+
if (!appDir) {
|
|
1980
|
+
subProjectRoot = path$1.resolve(root, './src/');
|
|
1981
|
+
}
|
|
1982
|
+
return subProjectRoot;
|
|
1983
|
+
}
|
|
1984
|
+
function getSubProjectConfig(subProjectRoot) {
|
|
1985
|
+
let res = {};
|
|
1986
|
+
try {
|
|
1987
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1988
|
+
res = require(path$1.resolve(subProjectRoot, 'config.js'));
|
|
1989
|
+
}
|
|
1990
|
+
catch (err) { }
|
|
1991
|
+
return res;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
const apikey = 'c0zWN3rC0FT4vlvhZYynvy689NxB3kq6';
|
|
1995
|
+
tinify.key = apikey;
|
|
1996
|
+
/**
|
|
1997
|
+
* 从 Rainbow 获取 TinyPNG API Keys
|
|
1998
|
+
* @returns API Keys 列表
|
|
1999
|
+
*/
|
|
2000
|
+
async function fetchApiKeys() {
|
|
2001
|
+
const result = await fetchRainbowConfigFromSdk({
|
|
2002
|
+
secretInfo: {
|
|
2003
|
+
appId: process.env.RAINBOW_APP_ID,
|
|
2004
|
+
userId: process.env.RAINBOW_USER_ID || '',
|
|
2005
|
+
secretKey: process.env.RAINBOW_SECRET_KEY || '',
|
|
2006
|
+
envName: 'Default',
|
|
2007
|
+
groupName: 'devops',
|
|
2008
|
+
},
|
|
2009
|
+
key: 'tiny_png_api_keys',
|
|
2010
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
2011
|
+
sdk: require(' '),
|
|
2012
|
+
tryJsonParse: true,
|
|
2013
|
+
});
|
|
2014
|
+
const list = result.data.map((item) => item.key);
|
|
2015
|
+
return list;
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* 使用指定 Key 尝试压缩文件
|
|
2019
|
+
* @param file - 输入文件
|
|
2020
|
+
* @param key - TinyPNG API Key
|
|
2021
|
+
* @returns 压缩后的文件路径或缓冲区
|
|
2022
|
+
*/
|
|
2023
|
+
async function tryCompressUseEachKey(file, key) {
|
|
2024
|
+
if (file.path && !file.buffer) {
|
|
2025
|
+
tinify.key = key;
|
|
2026
|
+
const list = file.path.split(path.sep);
|
|
2027
|
+
const newPath = `${list.slice(0, list.length - 1).join(path.sep) + path.sep}tiny_${list[list.length - 1]}`;
|
|
2028
|
+
console.log('newPath', newPath);
|
|
2029
|
+
await tinify.fromFile(file.path).toFile(newPath);
|
|
2030
|
+
return Promise.resolve(newPath);
|
|
2031
|
+
}
|
|
2032
|
+
return new Promise((resolve, reject) => {
|
|
2033
|
+
tinify.key = key;
|
|
2034
|
+
tinify
|
|
2035
|
+
.fromBuffer(file.buffer)
|
|
2036
|
+
.toBuffer((err, resultData) => {
|
|
2037
|
+
console.log('resultData', { resultData, err }, resultData);
|
|
2038
|
+
if (err) {
|
|
2039
|
+
reject(err);
|
|
2040
|
+
}
|
|
2041
|
+
else if (resultData) {
|
|
2042
|
+
resolve(Buffer.from(resultData));
|
|
2043
|
+
}
|
|
2044
|
+
else {
|
|
2045
|
+
reject(new Error('tinypng 压缩结果为空'));
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* 使用 TinyPNG 压缩文件
|
|
2052
|
+
* @param file - 输入文件
|
|
2053
|
+
* @returns 压缩后的文件路径或缓冲区
|
|
2054
|
+
* @throws 如果所有 API Key 都失败则抛出错误
|
|
2055
|
+
*/
|
|
2056
|
+
async function compressFileUseTinypng(file) {
|
|
2057
|
+
const apikeys = await fetchApiKeys();
|
|
2058
|
+
console.log('apikeysMap', apikeys);
|
|
2059
|
+
let err;
|
|
2060
|
+
let result;
|
|
2061
|
+
for (const key of apikeys) {
|
|
2062
|
+
try {
|
|
2063
|
+
result = await tryCompressUseEachKey(file, key);
|
|
2064
|
+
if (result) {
|
|
2065
|
+
return result;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
catch (e) {
|
|
2069
|
+
err = e;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
if (!result && err) {
|
|
2073
|
+
throw new Error(err.message);
|
|
2074
|
+
}
|
|
2075
|
+
return result;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
const getPlatform = () => process.env.UNI_PLATFORM || '';
|
|
2079
|
+
const getUtsPlatform = () => process.env.UNI_UTS_PLATFORM || '';
|
|
2080
|
+
const getAppPlatform = () => process.env.UNI_APP_PLATFORM || '';
|
|
2081
|
+
const isH5 = () => getPlatform() === 'h5';
|
|
2082
|
+
const isApp = () => getPlatform() === 'app';
|
|
2083
|
+
const isAppAndroid = () => getAppPlatform() === 'android' || getUtsPlatform() === 'app-android';
|
|
2084
|
+
const isAppIOS = () => getAppPlatform() === 'ios' || getUtsPlatform() === 'app-ios';
|
|
2085
|
+
const isMp = () => /^mp-/i.test(getPlatform());
|
|
2086
|
+
const isMpWeixin = () => getPlatform() === 'mp-weixin';
|
|
2087
|
+
const isMpAlipay = () => getPlatform() === 'mp-alipay';
|
|
2088
|
+
const isMpBaidu = () => getPlatform() === 'mp-baidu';
|
|
2089
|
+
const isMpKuaishou = () => getPlatform() === 'mp-kuaishou';
|
|
2090
|
+
const isMpQQ = () => getPlatform() === 'mp-qq';
|
|
2091
|
+
const isMpToutiao = () => getPlatform() === 'mp-toutiao';
|
|
2092
|
+
const isQuickapp = () => /^quickapp-webview/i.test(getPlatform());
|
|
2093
|
+
const isQuickappUnion = () => getPlatform() === 'quickapp-webview-union';
|
|
2094
|
+
const isQuickappHuawei = () => getPlatform() === 'quickapp-webview-huawei';
|
|
2095
|
+
|
|
2096
|
+
const htmlReg = /(?<=<template>)([\s\S]+)(?=<\/template>)/;
|
|
2097
|
+
const imgReg = /(<img[\s\S]+?)v-lazy=(?:"|')(.*?)(?:"|')([\s\S]*?>)/g;
|
|
2098
|
+
const sizeReg = /(?<=[\s\n]+(?:data-)?)size=(?:"|')(\d+)(?:"|')/;
|
|
2099
|
+
const widthReg = /(?<=[\s\n]+(?:data-)?)width=(?:"|')(\d+)(?:"|')/;
|
|
2100
|
+
const heightReg = /(?<=[\s\n]+(?:data-)?)height=(?:"|')(\d+)(?:"|')/;
|
|
2101
|
+
function vLazyCore(source, options) {
|
|
2102
|
+
const { urlHandler } = options || {};
|
|
2103
|
+
let html = '';
|
|
2104
|
+
const match = source.match(htmlReg);
|
|
2105
|
+
if (match?.[1]) {
|
|
2106
|
+
html = match[1];
|
|
2107
|
+
}
|
|
2108
|
+
if (!html)
|
|
2109
|
+
return source;
|
|
2110
|
+
if (!html.match(imgReg))
|
|
2111
|
+
return source;
|
|
2112
|
+
const newHtml = handleImg(html, urlHandler);
|
|
2113
|
+
const newSource = source.replace(htmlReg, () => newHtml);
|
|
2114
|
+
return newSource;
|
|
2115
|
+
}
|
|
2116
|
+
function getSize(pre, post, reg) {
|
|
2117
|
+
let size = '';
|
|
2118
|
+
const preMatch = pre.match(reg);
|
|
2119
|
+
const postMatch = post.match(reg);
|
|
2120
|
+
if (preMatch?.[1]) {
|
|
2121
|
+
size = preMatch[1];
|
|
2122
|
+
}
|
|
2123
|
+
else if (postMatch?.[1]) {
|
|
2124
|
+
size = postMatch[1];
|
|
2125
|
+
}
|
|
2126
|
+
return size;
|
|
2127
|
+
}
|
|
2128
|
+
function getImgSrc({ urlHandler, src, size, width, height, }) {
|
|
2129
|
+
let srcStr = src;
|
|
2130
|
+
if (!urlHandler) {
|
|
2131
|
+
return src;
|
|
2132
|
+
}
|
|
2133
|
+
if (width && height) {
|
|
2134
|
+
srcStr = `${urlHandler}(${src}, ${width}, ${height})`;
|
|
2135
|
+
}
|
|
2136
|
+
else if (size) {
|
|
2137
|
+
srcStr = `${urlHandler}(${src}, ${size}, ${size})`;
|
|
2138
|
+
}
|
|
2139
|
+
else {
|
|
2140
|
+
srcStr = `${urlHandler}(${src})`;
|
|
2141
|
+
}
|
|
2142
|
+
return srcStr;
|
|
2143
|
+
}
|
|
2144
|
+
function handleImg(str = '', urlHandler = '') {
|
|
2145
|
+
const res = str.replace(imgReg, (...args) => {
|
|
2146
|
+
const { 1: pre, 2: src, 3: post } = args;
|
|
2147
|
+
const size = getSize(pre, post, sizeReg);
|
|
2148
|
+
const width = getSize(pre, post, widthReg);
|
|
2149
|
+
const height = getSize(pre, post, heightReg);
|
|
2150
|
+
const srcStr = getImgSrc({
|
|
2151
|
+
urlHandler,
|
|
2152
|
+
src,
|
|
2153
|
+
size,
|
|
2154
|
+
width,
|
|
2155
|
+
height,
|
|
2156
|
+
});
|
|
2157
|
+
const innerRes = `${pre} :src="${srcStr}" lazy-load ${post}`;
|
|
2158
|
+
return innerRes;
|
|
2159
|
+
});
|
|
2160
|
+
return res;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/**
|
|
2164
|
+
* 白名单用户状态映射
|
|
2165
|
+
*/
|
|
2166
|
+
const WHITE_USER_STATUS_MAP = {
|
|
2167
|
+
/** 待处理 */
|
|
2168
|
+
PENDING: 0,
|
|
2169
|
+
/** 成功 */
|
|
2170
|
+
SUCCESS: 1,
|
|
2171
|
+
/** 失败 */
|
|
2172
|
+
FAIL: 2,
|
|
2173
|
+
};
|
|
2174
|
+
/**
|
|
2175
|
+
* 白名单用户流水线 ID
|
|
2176
|
+
*/
|
|
2177
|
+
const WHITE_USER_PIPELINE_ID = 'p-cd935542953b4057884920da801d9feb';
|
|
2178
|
+
/**
|
|
2179
|
+
* 白名单用户 CDN 刷新流水线 ID
|
|
2180
|
+
*/
|
|
2181
|
+
const WHITE_USER_CDN_REFRESH_PIPELINE_ID = 'p-24d52115511f4fb8bacdd95114ff044d';
|
|
2182
|
+
/**
|
|
2183
|
+
* Rainbow 配置常量
|
|
2184
|
+
*/
|
|
2185
|
+
const RAINBOW_CONFIG = {
|
|
2186
|
+
configKey: 'frontend_white_config',
|
|
2187
|
+
envName: 'Default',
|
|
2188
|
+
groupName: 'devops_open',
|
|
2189
|
+
};
|
|
2190
|
+
/**
|
|
2191
|
+
* 获取白名单 Rainbow 配置
|
|
2192
|
+
* @returns 白名单配置对象
|
|
2193
|
+
*/
|
|
2194
|
+
async function getWhiteRainbowConfig() {
|
|
2195
|
+
const { RAINBOW_APP_ID, } = process.env;
|
|
2196
|
+
const configListStr = await fetchRainbowConfigFromSdk({
|
|
2197
|
+
secretInfo: {
|
|
2198
|
+
appID: RAINBOW_APP_ID,
|
|
2199
|
+
userId: process.env.RAINBOW_USER_ID || '',
|
|
2200
|
+
secretKey: process.env.RAINBOW_SECRET_KEY || '',
|
|
2201
|
+
envName: RAINBOW_CONFIG.envName,
|
|
2202
|
+
groupName: RAINBOW_CONFIG.groupName,
|
|
2203
|
+
},
|
|
2204
|
+
key: RAINBOW_CONFIG.configKey,
|
|
2205
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
2206
|
+
sdk: require(' '),
|
|
2207
|
+
rainbow: global.rainbowInstance,
|
|
2208
|
+
tryJsonParse: false,
|
|
2209
|
+
initOptions: {
|
|
2210
|
+
isUsingFileCache: false,
|
|
2211
|
+
isUsingLocalCache: false,
|
|
2212
|
+
},
|
|
2213
|
+
});
|
|
2214
|
+
const config = safeJsonParse(configListStr || '{}', {
|
|
2215
|
+
whiteTypeList: [],
|
|
2216
|
+
whiteTypeMap: {},
|
|
2217
|
+
});
|
|
2218
|
+
const whiteTypeList = config?.whiteTypeList || [];
|
|
2219
|
+
const whiteTypeMap = whiteTypeList.reduce((acc, item) => ({
|
|
2220
|
+
...acc,
|
|
2221
|
+
[item.value]: item,
|
|
2222
|
+
}), {});
|
|
2223
|
+
return {
|
|
2224
|
+
...(config || {}),
|
|
2225
|
+
whiteTypeMap,
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* 获取白名单类型描述
|
|
2230
|
+
* @param whiteType - 白名单类型
|
|
2231
|
+
* @returns 白名单类型的显示标签
|
|
2232
|
+
*/
|
|
2233
|
+
async function getWhiteTypeDesc(whiteType) {
|
|
2234
|
+
const config = await getWhiteRainbowConfig();
|
|
2235
|
+
const whiteTypeMap = config?.whiteTypeMap || {};
|
|
2236
|
+
return whiteTypeMap[whiteType]?.label;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
/**
|
|
2240
|
+
* 获取两个列表的差异项
|
|
2241
|
+
* @param config - 新配置列表
|
|
2242
|
+
* @param originConfig - 原配置列表
|
|
2243
|
+
* @returns 差异项列表
|
|
2244
|
+
*/
|
|
2245
|
+
function getEachDiff(config = [], originConfig = []) {
|
|
2246
|
+
return config.filter(item => !originConfig.includes(item));
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* 获取文件链接
|
|
2250
|
+
* @param params - 文件参数
|
|
2251
|
+
* @returns 完整的文件 URL
|
|
2252
|
+
*/
|
|
2253
|
+
function getFileLink({ fileName, cdn, }) {
|
|
2254
|
+
if (cdn) {
|
|
2255
|
+
const fullName = `https://${cdn}/white/${fileName}.json?v=${Date.now()}`;
|
|
2256
|
+
return fullName;
|
|
2257
|
+
}
|
|
2258
|
+
const fullName = `https://tip-components-1251917893.cos.ap-guangzhou.myqcloud.com/white/${fileName}.json`;
|
|
2259
|
+
return fullName;
|
|
2260
|
+
}
|
|
2261
|
+
/**
|
|
2262
|
+
* 获取文件差异
|
|
2263
|
+
* @param params - 文件参数
|
|
2264
|
+
* @returns 差异结果
|
|
2265
|
+
*/
|
|
2266
|
+
async function getDiff({ fileName, fileContent, cdn, }) {
|
|
2267
|
+
const fullName = getFileLink({
|
|
2268
|
+
fileName,
|
|
2269
|
+
cdn,
|
|
2270
|
+
});
|
|
2271
|
+
let originList = [];
|
|
2272
|
+
try {
|
|
2273
|
+
const origin = await axios.get(fullName);
|
|
2274
|
+
originList = origin?.data?.data || [];
|
|
2275
|
+
}
|
|
2276
|
+
catch (err) {
|
|
2277
|
+
// 忽略错误
|
|
2278
|
+
}
|
|
2279
|
+
const addedList = getEachDiff(fileContent, originList);
|
|
2280
|
+
const deletedList = getEachDiff(originList, fileContent);
|
|
2281
|
+
return {
|
|
2282
|
+
deletedList,
|
|
2283
|
+
addedList,
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
/**
|
|
2287
|
+
* 发送企业微信机器人消息
|
|
2288
|
+
* @param options - 消息选项
|
|
2289
|
+
*/
|
|
2290
|
+
async function sendRobotMessage({ addedList, deletedList, fileName, label, fromCron, fromWebhook, found, }) {
|
|
2291
|
+
if (!addedList?.length && !deletedList?.length) {
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
const beauty = (list) => list.map(item => `\`${item}\``).join(',');
|
|
2295
|
+
const getTrigger = () => {
|
|
2296
|
+
if (fromCron) {
|
|
2297
|
+
return '定时任务';
|
|
2298
|
+
}
|
|
2299
|
+
if (fromWebhook) {
|
|
2300
|
+
return '回调';
|
|
2301
|
+
}
|
|
2302
|
+
return '未知';
|
|
2303
|
+
};
|
|
2304
|
+
const applyUserStr = fromWebhook && found?.length ? `,申请者<@${found[0].applyUser}>` : '';
|
|
2305
|
+
const content = [
|
|
2306
|
+
`【前端白名单更新】\`${label || fileName}\`,触发自\`Next SVR - ${getTrigger()}\`,[自助申请地址](https://mobile.aow.com/rd-platform-web/#/management/white/new/)${applyUserStr}`,
|
|
2307
|
+
addedList.length ? `- 新增:${beauty(addedList)}` : '',
|
|
2308
|
+
deletedList.length ? `- 删除:${beauty(deletedList)}` : '',
|
|
2309
|
+
]
|
|
2310
|
+
.filter(item => item)
|
|
2311
|
+
.join('\n');
|
|
2312
|
+
await batchSendWxRobotMarkdown({
|
|
2313
|
+
content,
|
|
2314
|
+
chatId: 'ALL',
|
|
2315
|
+
webhookUrl: '8d47cdb4-6852-4305-aefd-17b20478ae4e',
|
|
2316
|
+
// only me
|
|
2317
|
+
// webhookUrl: 'd7ac7b67-0960-4b15-a407-6d682ba77247',
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* 根据数据库更新白名单用户 COS 文件
|
|
2322
|
+
* @param options - 更新选项
|
|
2323
|
+
*/
|
|
2324
|
+
async function updateWhiteUserCosByDb(options) {
|
|
2325
|
+
const { buildId, fromCron = false, WhiteUserDB, startPipeline, } = options || {};
|
|
2326
|
+
const where = {
|
|
2327
|
+
status: 1,
|
|
2328
|
+
};
|
|
2329
|
+
let found = [];
|
|
2330
|
+
if (buildId) {
|
|
2331
|
+
found = await WhiteUserDB.getList({
|
|
2332
|
+
start: 0,
|
|
2333
|
+
limit: 999999,
|
|
2334
|
+
where: {
|
|
2335
|
+
pipelineRunId: buildId,
|
|
2336
|
+
},
|
|
2337
|
+
});
|
|
2338
|
+
if (found?.[0]) {
|
|
2339
|
+
where.whiteType = found[0].whiteType;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
const list = await WhiteUserDB.getList({
|
|
2343
|
+
start: 0,
|
|
2344
|
+
limit: 999999,
|
|
2345
|
+
where,
|
|
2346
|
+
});
|
|
2347
|
+
const config = await getWhiteRainbowConfig();
|
|
2348
|
+
const files = await generateCosFile(list, config);
|
|
2349
|
+
for (const item of files) {
|
|
2350
|
+
const { addedList, deletedList, } = await getDiff(item);
|
|
2351
|
+
if (!addedList?.length && !deletedList.length) {
|
|
2352
|
+
continue;
|
|
2353
|
+
}
|
|
2354
|
+
await sendRobotMessage({
|
|
2355
|
+
addedList,
|
|
2356
|
+
deletedList,
|
|
2357
|
+
fileName: item.fileName,
|
|
2358
|
+
label: item.label,
|
|
2359
|
+
fromCron,
|
|
2360
|
+
fromWebhook: !!buildId,
|
|
2361
|
+
found,
|
|
2362
|
+
});
|
|
2363
|
+
await uploadToCos(item);
|
|
2364
|
+
await startPipeline(WHITE_USER_CDN_REFRESH_PIPELINE_ID, {});
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* 保存文件到本地
|
|
2369
|
+
* @param fileName - 文件名
|
|
2370
|
+
* @param fileContent - 文件内容
|
|
2371
|
+
* @returns 保存后的文件路径
|
|
2372
|
+
*/
|
|
2373
|
+
function saveFile(fileName, fileContent) {
|
|
2374
|
+
const targetDir = path.resolve(__dirname, '../../../log');
|
|
2375
|
+
if (!fs.existsSync(targetDir)) {
|
|
2376
|
+
fs.mkdirSync(targetDir);
|
|
2377
|
+
}
|
|
2378
|
+
const targetFile = path.resolve(targetDir, fileName);
|
|
2379
|
+
writeFileSync(targetFile, { data: fileContent }, true);
|
|
2380
|
+
return targetFile;
|
|
2381
|
+
}
|
|
2382
|
+
/**
|
|
2383
|
+
* 上传文件到 COS
|
|
2384
|
+
* @param file - 文件信息
|
|
2385
|
+
* @returns 上传结果
|
|
2386
|
+
*/
|
|
2387
|
+
async function uploadToCos(file) {
|
|
2388
|
+
const logFilename = `${file.fileName}.json`;
|
|
2389
|
+
const targetFile = await saveFile(logFilename, file.fileContent);
|
|
2390
|
+
const newFileKey = `white/${logFilename}`;
|
|
2391
|
+
if (file.cdn) {
|
|
2392
|
+
const cos = getUploadCosConfig(file.cdn);
|
|
2393
|
+
if (!cos) {
|
|
2394
|
+
throw new Error(`cdn not found: ${file.cdn}`);
|
|
2395
|
+
}
|
|
2396
|
+
await uploadCOSFile({
|
|
2397
|
+
secretId: cos.secretId,
|
|
2398
|
+
secretKey: cos.secretKey,
|
|
2399
|
+
bucket: cos.bucket,
|
|
2400
|
+
region: cos.region,
|
|
2401
|
+
files: [
|
|
2402
|
+
{
|
|
2403
|
+
key: newFileKey,
|
|
2404
|
+
path: targetFile,
|
|
2405
|
+
},
|
|
2406
|
+
],
|
|
2407
|
+
}).then((res) => {
|
|
2408
|
+
console.log('[uploadCOSFile] res: ', res);
|
|
2409
|
+
})
|
|
2410
|
+
.catch((err) => {
|
|
2411
|
+
console.log('[uploadCOSFile] err: ', err);
|
|
2412
|
+
});
|
|
2413
|
+
const url = `https://${cos.bucket}.cos.${cos.region}.myqcloud.com/${newFileKey}`;
|
|
2414
|
+
purgeUrlCache({
|
|
2415
|
+
secretId: cos.secretId,
|
|
2416
|
+
secretKey: cos.secretKey,
|
|
2417
|
+
urls: [toCdnUrl(url)],
|
|
2418
|
+
area: getPushUrlCacheArea(file.cdn),
|
|
2419
|
+
})
|
|
2420
|
+
.then((res) => {
|
|
2421
|
+
console.log('[purgeUrlCache] res: ', res?.data);
|
|
2422
|
+
})
|
|
2423
|
+
.catch((err) => {
|
|
2424
|
+
console.log('[purgeUrlCache] err: ', err);
|
|
2425
|
+
});
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
return await uploadCOSFile({
|
|
2429
|
+
secretId: process.env.CLOUD_API_SECRET_ID || '',
|
|
2430
|
+
secretKey: process.env.CLOUD_API_SECRET_KEY || '',
|
|
2431
|
+
bucket: 'tip-components-1251917893',
|
|
2432
|
+
region: 'ap-guangzhou',
|
|
2433
|
+
files: [
|
|
2434
|
+
{
|
|
2435
|
+
key: newFileKey,
|
|
2436
|
+
path: targetFile,
|
|
2437
|
+
},
|
|
2438
|
+
],
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* 生成 COS 文件内容
|
|
2443
|
+
* @param list - 白名单用户列表
|
|
2444
|
+
* @param config - 白名单配置
|
|
2445
|
+
* @returns 文件信息列表
|
|
2446
|
+
*/
|
|
2447
|
+
function generateCosFile(list, config) {
|
|
2448
|
+
const targetMap = list
|
|
2449
|
+
.filter((item) => {
|
|
2450
|
+
const valid = item.startTime + item.validTime * 1000 > Date.now();
|
|
2451
|
+
return valid;
|
|
2452
|
+
})
|
|
2453
|
+
.reduce((acc, item) => ({
|
|
2454
|
+
...acc,
|
|
2455
|
+
[item.whiteType]: [
|
|
2456
|
+
...(acc[item.whiteType] || []),
|
|
2457
|
+
item,
|
|
2458
|
+
],
|
|
2459
|
+
}), {});
|
|
2460
|
+
const files = Object.keys(targetMap)
|
|
2461
|
+
.map((key) => {
|
|
2462
|
+
const fileName = config?.whiteTypeMap?.[key]?.file;
|
|
2463
|
+
const fileContent = targetMap[key].map(item => item.openId);
|
|
2464
|
+
const label = config?.whiteTypeMap?.[key]?.label;
|
|
2465
|
+
const cdn = config?.whiteTypeMap?.[key]?.cdn;
|
|
2466
|
+
return {
|
|
2467
|
+
fileName: fileName || '',
|
|
2468
|
+
fileContent,
|
|
2469
|
+
label,
|
|
2470
|
+
cdn,
|
|
2471
|
+
};
|
|
2472
|
+
})
|
|
2473
|
+
.filter(item => item.fileName);
|
|
2474
|
+
return files || [];
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
/**
|
|
2478
|
+
* 定时任务执行时间(每 10 分钟执行一次)
|
|
2479
|
+
*/
|
|
2480
|
+
const CRON_TIME = '0 */10 * * * *';
|
|
2481
|
+
/**
|
|
2482
|
+
* 创建更新白名单用户 COS 的定时任务
|
|
2483
|
+
* @param params - 定时任务参数
|
|
2484
|
+
*/
|
|
2485
|
+
function createUpdateWhiteUserCosCron({ WhiteUserDB, startPipeline, }) {
|
|
2486
|
+
if (process.env.NODE_ENV === 'development') {
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
try {
|
|
2490
|
+
new Cron(CRON_TIME, {
|
|
2491
|
+
timezone: 'Asia/Shanghai',
|
|
2492
|
+
}, () => {
|
|
2493
|
+
console.log('[CRON] updateCosByDb', timeStampFormat(Date.now(), 'yyyy-MM-dd hh:mm:ss'));
|
|
2494
|
+
updateWhiteUserCosByDb({
|
|
2495
|
+
fromCron: true,
|
|
2496
|
+
WhiteUserDB,
|
|
2497
|
+
startPipeline,
|
|
2498
|
+
});
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
catch (err) {
|
|
2502
|
+
// 忽略错误
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
export { AEGIS_EXTERNAL_SCRIPT_LINK, ALL_PLATFORM, BASE_SCSS, CDN_MAP, COS_BASE, COS_TO_GIT_REMOTE_PIPELINE_ID, CSS_MAP, CSS_POSTFIX_MAP, DEFAULT_ADAPTER_DIRS, DEFAULT_CONTEXT_OBJECT, DEFAULT_TRANSPILE_DEPENDENCIES, EXTERNAL_LINK_MAP, FONT_PROJECT_CDN, FONT_PROJECT_COS_BASE, FONT_PROJECT_WEBHOOK_URL, HTML_MAP, ICON_PROJECT_CDN, ICON_PROJECT_PIPELINE_CONFIG, ICON_PROJECT_WEBHOOK_URL, MAINLAND_CDN_LIST, PLATFORMS_ALL, PLATFORMS_MP, PLATFORM_MAP, ROOT_NAME, TEMPLATE_ID_MAP, UNI_SIMPLE_ROUTER_SCRIPT_LINK, WHITE_USER_CDN_REFRESH_PIPELINE_ID, WHITE_USER_PIPELINE_ID, WHITE_USER_STATUS_MAP, addMPPublishRecord, callRdMCPApi, checkBundleAnalyze, checkDebugMode, checkH5, compressFileUseTinypng, cosSyncToGit, createFontProjectOrUpdate, createIconProjectOrUpdate, createLogDir, createUpdateWhiteUserCosCron, crossGameStyle, extractRepoPath, findDependencies, findNodeModuleFile, genFontProjects, genIconProjects, genInjectContent, getAllMpPipeline, getAppPlatform, getCommitCode, getComponentByRepo, getComponentName, getDeps, getEnvByBranch, getGenVersionPluginOptions, getImportOrderRule, getLoaderFile, getLoaderProdFile, getMcpName, getMcpVersion, getMpCICommonConfig, getMpCIKeyConfig, getMpInsertCode, getMpVersionCode, getOneCdnUrl, getPipelineRunLink, getPlatform, getProjectName, getPushUrlCacheArea, getRainbowConfig, getRelativePath, getRootDir, getStyleList, getSubProjectConfig, getSubProjectName, getSubProjectRoot, getUploadCosConfig, getUploadImageFilePath, getUtsPlatform, getVersionCode, getWebInsertCode, getWhiteRainbowConfig, getWhiteTypeDesc, getWxmlAndWxssPostfix, handleAxiosError, isApp, isAppAndroid, isAppIOS, isH5, isInBlackList, isMp, isMpAlipay, isMpBaidu, isMpKuaishou, isMpQQ, isMpToutiao, isMpWeixin, isQuickapp, isQuickappHuawei, isQuickappUnion, normalizePath, parseImage, parseMCPCommandLineArgs, parsePublishReason, parseSetDeps, recordLoaderLog, removeFirstSlash, replaceDirective, revertManifest, saveJsonToLog, saveLoaderLog, scssLogger, shouldUseLoader, startMpCIPipeline, startNPMPublishPipelineCore, startPipelineByRawApi, toCdnUrl, updateAssetSource, updateManifest, updateMpCiConfig, updateWhiteUserCosByDb, uploadFilesCore, vLazyCore, validateMCPCredentials, watchEmails };
|