@jayfong/x-utils 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ai.d.mts +103 -0
- package/lib/ai.mjs +75 -0
- package/lib/cache.d.mts +464 -0
- package/lib/cache.mjs +531 -0
- package/lib/captcha.d.mts +259 -0
- package/lib/captcha.mjs +174 -0
- package/lib/chinese.d.mts +19 -1
- package/lib/chinese.mjs +28 -1
- package/lib/cli.d.mts +25 -37
- package/lib/cli.mjs +25 -22
- package/lib/crawler.d.mts +1 -1
- package/lib/crawler.mjs +1 -1
- package/lib/crypto.d.mts +158 -0
- package/lib/crypto.mjs +213 -0
- package/lib/data_set.mjs +3 -3
- package/lib/db_value.d.mts +213 -0
- package/lib/db_value.mjs +208 -0
- package/lib/dispose.d.mts +98 -0
- package/lib/dispose.mjs +107 -0
- package/lib/file_store.d.mts +167 -0
- package/lib/file_store.mjs +120 -0
- package/lib/fs.d.mts +64 -2
- package/lib/fs.mjs +65 -3
- package/lib/html.d.mts +103 -0
- package/lib/html.mjs +112 -0
- package/lib/jwt.d.mts +128 -0
- package/lib/jwt.mjs +104 -0
- package/lib/mail.d.mts +125 -0
- package/lib/mail.mjs +108 -0
- package/lib/pay.d.mts +394 -0
- package/lib/pay.mjs +343 -0
- package/lib/puppeteer.mjs +3 -3
- package/lib/random.d.mts +71 -0
- package/lib/random.mjs +92 -0
- package/lib/redis.d.mts +32 -0
- package/lib/redis.mjs +24 -0
- package/lib/runner.d.mts +52 -24
- package/lib/runner.mjs +31 -15
- package/lib/sensitive_words.d.mts +82 -0
- package/lib/sensitive_words.mjs +73 -0
- package/lib/sms.d.mts +133 -0
- package/lib/sms.mjs +100 -0
- package/lib/task.d.mts +1 -1
- package/lib/task.mjs +4 -4
- package/package.json +35 -3
package/lib/pay.mjs
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { xr } from "@jayfong/x-request";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { AlipaySdk } from "alipay-sdk";
|
|
4
|
+
// src/pay.ts
|
|
5
|
+
/**
|
|
6
|
+
* 支付工具。
|
|
7
|
+
*
|
|
8
|
+
* 封装支付宝(网页支付)与微信支付(JSAPI 支付)的下单、回调验签与
|
|
9
|
+
* 订单查询能力:
|
|
10
|
+
* - 支付宝:`prepareAlipay` 生成支付链接、`getOrderInfo` 查询订单;
|
|
11
|
+
* - 微信支付:`prepareWepay` 生成 JSSDK 支付参数、`getOrderInfo` 查询订单;
|
|
12
|
+
* - 回调:`verifyNotifyData` 统一校验并解析两个渠道的回调数据。
|
|
13
|
+
*
|
|
14
|
+
* 支付宝 SDK 按需惰性创建,只有首次使用支付宝能力时才初始化。
|
|
15
|
+
* 注意:微信支付回调目前只做 AES-GCM 解密,未实现签名验签,
|
|
16
|
+
* 业务方如需严格校验身份,请自行补充。
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const pay = new XPay({
|
|
20
|
+
* alipay: {
|
|
21
|
+
* appId: '2021000000000000',
|
|
22
|
+
* privateKey: '应用私钥',
|
|
23
|
+
* publicKey: '支付宝公钥',
|
|
24
|
+
* },
|
|
25
|
+
* wepay: {
|
|
26
|
+
* appId: 'wx1234567890',
|
|
27
|
+
* merchantId: '1234567890',
|
|
28
|
+
* privateKey: '商户 API 证书私钥',
|
|
29
|
+
* certificateSerialNumber: '证书序列号',
|
|
30
|
+
* secretKey: '32 字节 APIv3 密钥',
|
|
31
|
+
* },
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* const { payUrl } = await pay.prepareAlipay({
|
|
35
|
+
* goodsName: '测试商品',
|
|
36
|
+
* tradeNumber: 'T20260101001',
|
|
37
|
+
* totalMoney: 0.01,
|
|
38
|
+
* notifyUrl: 'https://example.com/notify/alipay',
|
|
39
|
+
* });
|
|
40
|
+
* // 跳转到 payUrl 完成支付
|
|
41
|
+
*/
|
|
42
|
+
var XPay = class {
|
|
43
|
+
options;
|
|
44
|
+
alipaySdk;
|
|
45
|
+
constructor(options) {
|
|
46
|
+
this.options = options;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 支付宝网页支付下单,生成支付链接。
|
|
50
|
+
*
|
|
51
|
+
* 调用支付宝 `alipay.trade.page.pay` 接口生成 GET 形式的支付地址,
|
|
52
|
+
* 用户跳转到该地址即可在支付宝收银台完成支付。订单金额会格式化为
|
|
53
|
+
* 两位小数字符串,并通过 `passback_params` 标记渠道为 `alipay`,
|
|
54
|
+
* 以便回调时识别。
|
|
55
|
+
*
|
|
56
|
+
* @param options 下单参数,见 {@link XPayPrepareAlipayOptions}。
|
|
57
|
+
* @returns 支付地址,见 {@link XPayPrepareAlipayResult}。
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* const { payUrl } = await pay.prepareAlipay({
|
|
61
|
+
* goodsName: '测试商品',
|
|
62
|
+
* tradeNumber: 'T20260101001',
|
|
63
|
+
* totalMoney: 0.01,
|
|
64
|
+
* notifyUrl: 'https://example.com/notify/alipay',
|
|
65
|
+
* });
|
|
66
|
+
*/
|
|
67
|
+
async prepareAlipay(options) {
|
|
68
|
+
return { payUrl: this.getAlipaySdk().pageExecute("alipay.trade.page.pay", "GET", {
|
|
69
|
+
notifyUrl: options.notifyUrl,
|
|
70
|
+
...options.returnUrl ? { returnUrl: options.returnUrl } : {},
|
|
71
|
+
bizContent: {
|
|
72
|
+
outTradeNo: options.tradeNumber,
|
|
73
|
+
productCode: "FAST_INSTANT_TRADE_PAY",
|
|
74
|
+
totalAmount: options.totalMoney.toFixed(2),
|
|
75
|
+
subject: options.goodsName,
|
|
76
|
+
goodsType: "0",
|
|
77
|
+
passbackParams: "alipay"
|
|
78
|
+
}
|
|
79
|
+
}) };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 微信支付 JSAPI 下单,生成 JSSDK 支付参数。
|
|
83
|
+
*
|
|
84
|
+
* 调用微信支付 `v3/pay/transactions/jsapi` 接口获取 `prepay_id`,
|
|
85
|
+
* 再按 JSSDK 规则签名得到支付参数,可直接传给前端
|
|
86
|
+
* `wx.requestPayment`。金额由元转换为分(乘以 100 后四舍五入),
|
|
87
|
+
* 避免浮点误差;下单请求会通过 `attach` 字段标记渠道为 `wepay`。
|
|
88
|
+
*
|
|
89
|
+
* @param options 下单参数,见 {@link XPayPrepareWepayOptions}。
|
|
90
|
+
* @returns 支付参数,见 {@link XPayPrepareWepayResult}。
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* const { payParams } = await pay.prepareWepay({
|
|
94
|
+
* goodsName: '测试商品',
|
|
95
|
+
* tradeNumber: 'T20260101002',
|
|
96
|
+
* totalMoney: 0.01,
|
|
97
|
+
* notifyUrl: 'https://example.com/notify/wepay',
|
|
98
|
+
* openid: 'oABC123',
|
|
99
|
+
* });
|
|
100
|
+
*/
|
|
101
|
+
async prepareWepay(options) {
|
|
102
|
+
const wepay = this.getWepayConfig();
|
|
103
|
+
const res = await this.wepayRequest("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi", {
|
|
104
|
+
appid: wepay.appId,
|
|
105
|
+
mchid: wepay.merchantId,
|
|
106
|
+
description: options.goodsName,
|
|
107
|
+
out_trade_no: options.tradeNumber,
|
|
108
|
+
attach: "wepay",
|
|
109
|
+
notify_url: options.notifyUrl,
|
|
110
|
+
amount: {
|
|
111
|
+
total: Math.round(options.totalMoney * 100),
|
|
112
|
+
currency: "CNY"
|
|
113
|
+
},
|
|
114
|
+
payer: { openid: options.openid }
|
|
115
|
+
});
|
|
116
|
+
if (!res || !res.prepay_id) throw new Error(res?.message || "微信支付下单失败,未返回 prepay_id");
|
|
117
|
+
const params = {
|
|
118
|
+
appId: wepay.appId,
|
|
119
|
+
timestamp: String(Math.round(Date.now() / 1e3)),
|
|
120
|
+
nonceStr: this.generateNonceStr(),
|
|
121
|
+
package: `prepay_id=${res.prepay_id}`,
|
|
122
|
+
signType: "RSA",
|
|
123
|
+
paySign: ""
|
|
124
|
+
};
|
|
125
|
+
params.paySign = this.wepaySign([
|
|
126
|
+
params.appId,
|
|
127
|
+
params.timestamp,
|
|
128
|
+
params.nonceStr,
|
|
129
|
+
params.package
|
|
130
|
+
]);
|
|
131
|
+
return { payParams: params };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 校验并解析支付回调通知数据。
|
|
135
|
+
*
|
|
136
|
+
* 先判断数据是否为对象,再按渠道标记识别:
|
|
137
|
+
* - 支付宝:当 `passback_params` 为 `alipay` 时,调用支付宝 SDK 验签,
|
|
138
|
+
* 验签通过后返回支付宝交易号;
|
|
139
|
+
* - 微信支付:当存在 `resource` 字段时,先 AES-GCM 解密,再校验
|
|
140
|
+
* `attach` 是否为 `wepay`,通过后返回微信支付订单号。
|
|
141
|
+
*
|
|
142
|
+
* 任何一步失败(数据非法、未配置渠道、验签失败、解密失败)都会
|
|
143
|
+
* 返回 `false`,不会抛出异常。
|
|
144
|
+
*
|
|
145
|
+
* @param data 支付平台 POST 过来的回调数据(支付宝为表单 JSON,
|
|
146
|
+
* 微信支付为通知报文 JSON)。
|
|
147
|
+
* @returns 校验通过时返回渠道订单号,否则返回 `false`。
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* const res = pay.verifyNotifyData(req.body);
|
|
151
|
+
* if (res) {
|
|
152
|
+
* // 校验通过,res.channelOrderNo 为渠道订单号
|
|
153
|
+
* }
|
|
154
|
+
*/
|
|
155
|
+
verifyNotifyData(data) {
|
|
156
|
+
try {
|
|
157
|
+
if (!data || typeof data !== "object") return false;
|
|
158
|
+
if (data.passback_params === "alipay") {
|
|
159
|
+
if (!this.getAlipaySdk().checkNotifySign(data)) return false;
|
|
160
|
+
return { channelOrderNo: data.trade_no };
|
|
161
|
+
}
|
|
162
|
+
if (data.resource) {
|
|
163
|
+
const params = JSON.parse(this.wepayDecrypt(data.resource));
|
|
164
|
+
if (params.attach === "wepay" && params.transaction_id) return { channelOrderNo: params.transaction_id };
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 校验并解析支付回调通知数据,失败时抛出错误。
|
|
174
|
+
*
|
|
175
|
+
* 行为与 {@link verifyNotifyData} 一致,区别在于校验失败时抛出
|
|
176
|
+
* `Error`,便于在需要强制校验的流程中使用。
|
|
177
|
+
*
|
|
178
|
+
* @param data 支付平台 POST 过来的回调数据。
|
|
179
|
+
* @param message 校验失败时抛出的错误信息。
|
|
180
|
+
* @returns 校验通过时的渠道订单号。
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* const res = pay.verifyNotifyDataOrFail(req.body, '回调验签失败');
|
|
184
|
+
*/
|
|
185
|
+
verifyNotifyDataOrFail(data, message) {
|
|
186
|
+
const res = this.verifyNotifyData(data);
|
|
187
|
+
if (!res) throw new Error(message);
|
|
188
|
+
return res;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 查询订单状态。
|
|
192
|
+
*
|
|
193
|
+
* 按渠道分别调用微信支付订单查询接口或支付宝 `alipay.trade.query`:
|
|
194
|
+
* - 优先使用 `orderNo`(商家订单号)查询,未提供时使用
|
|
195
|
+
* `channelOrderNo`(渠道订单号);
|
|
196
|
+
* - 微信支付 `trade_state` 为 `SUCCESS` 时视为成功;
|
|
197
|
+
* - 支付宝 `trade_status` 为 `TRADE_SUCCESS` 或 `TRADE_FINISHED` 时
|
|
198
|
+
* 视为成功,其余状态视为失败。
|
|
199
|
+
*
|
|
200
|
+
* @param payload 查询条件,见 {@link XPayGetOrderInfoPayload}。
|
|
201
|
+
* @returns 订单信息,见 {@link XPayGetOrderInfoResult}。
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* await pay.getOrderInfo({ channel: 'alipay', orderNo: 'T20260101001' });
|
|
205
|
+
* // { channelOrderNo: '2026...', orderNo: 'T20260101001', status: 'success' }
|
|
206
|
+
*/
|
|
207
|
+
async getOrderInfo(payload) {
|
|
208
|
+
if (payload.channel === "wepay") {
|
|
209
|
+
const wepay = this.getWepayConfig();
|
|
210
|
+
const res = await this.wepayRequest(payload.orderNo ? `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${payload.orderNo}?mchid=${wepay.merchantId}` : `https://api.mch.weixin.qq.com/v3/pay/transactions/id/${payload.channelOrderNo}?mchid=${wepay.merchantId}`);
|
|
211
|
+
return {
|
|
212
|
+
channelOrderNo: res.transaction_id,
|
|
213
|
+
orderNo: res.out_trade_no,
|
|
214
|
+
status: res.trade_state === "SUCCESS" ? "success" : "fail"
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const res = await this.getAlipaySdk().exec("alipay.trade.query", { biz_content: payload.orderNo ? { out_trade_no: payload.orderNo } : { trade_no: payload.channelOrderNo } });
|
|
218
|
+
return {
|
|
219
|
+
channelOrderNo: res.tradeNo,
|
|
220
|
+
orderNo: res.outTradeNo,
|
|
221
|
+
status: res.tradeStatus === "TRADE_SUCCESS" || res.tradeStatus === "TRADE_FINISHED" ? "success" : "fail"
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* 获取(并按需创建)支付宝 SDK 实例。
|
|
226
|
+
*
|
|
227
|
+
* 首次调用时才初始化 SDK,后续复用同一实例;未配置支付宝时抛出
|
|
228
|
+
* 明确错误。
|
|
229
|
+
*/
|
|
230
|
+
getAlipaySdk() {
|
|
231
|
+
const alipay = this.getAlipayConfig();
|
|
232
|
+
if (!this.alipaySdk) this.alipaySdk = new AlipaySdk({
|
|
233
|
+
appId: alipay.appId,
|
|
234
|
+
privateKey: alipay.privateKey,
|
|
235
|
+
alipayPublicKey: alipay.publicKey
|
|
236
|
+
});
|
|
237
|
+
return this.alipaySdk;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* 获取支付宝配置,未配置时抛出错误。
|
|
241
|
+
*/
|
|
242
|
+
getAlipayConfig() {
|
|
243
|
+
if (!this.options.alipay) throw new Error("未配置支付宝(alipay),无法使用支付宝相关能力");
|
|
244
|
+
return this.options.alipay;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* 获取微信支付配置,未配置时抛出错误。
|
|
248
|
+
*/
|
|
249
|
+
getWepayConfig() {
|
|
250
|
+
if (!this.options.wepay) throw new Error("未配置微信支付(wepay),无法使用微信支付相关能力");
|
|
251
|
+
return this.options.wepay;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* 生成随机字符串,用于微信支付请求的 `nonce_str` 与支付参数。
|
|
255
|
+
*
|
|
256
|
+
* 使用密码学安全的随机字节生成 12 位十六进制字符串。
|
|
257
|
+
*/
|
|
258
|
+
generateNonceStr() {
|
|
259
|
+
return crypto.randomBytes(6).toString("hex");
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 微信支付 RSA 签名。
|
|
263
|
+
*
|
|
264
|
+
* 将每个字段拼接换行符(`\n`)后使用 `RSA-SHA256` 私钥签名,返回
|
|
265
|
+
* base64 编码的签名;同一方法同时用于请求头签名与 JSSDK 支付参数
|
|
266
|
+
* 签名。
|
|
267
|
+
*
|
|
268
|
+
* @param data 待签名字段,按顺序依次拼接,每个字段后追加换行符。
|
|
269
|
+
* @returns base64 编码的签名串。
|
|
270
|
+
*/
|
|
271
|
+
wepaySign(data) {
|
|
272
|
+
return crypto.createSign("RSA-SHA256").update(data.map((v) => `${v}\n`).join("")).sign(this.getWepayConfig().privateKey, "base64");
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* 微信支付回调数据解密。
|
|
276
|
+
*
|
|
277
|
+
* 使用 APIv3 密钥对 `resource` 做 AES-256-GCM 解密:密文末尾 16
|
|
278
|
+
* 字节为认证标签,前面为加密数据,`nonce` 作为 IV,
|
|
279
|
+
* `associated_data` 作为附加认证数据。
|
|
280
|
+
*
|
|
281
|
+
* @param resource 回调数据中的 `resource` 字段。
|
|
282
|
+
* @returns 解密后的 JSON 字符串。
|
|
283
|
+
*/
|
|
284
|
+
wepayDecrypt(resource) {
|
|
285
|
+
const ciphertext = Buffer.from(resource.ciphertext, "base64");
|
|
286
|
+
const authTag = ciphertext.subarray(ciphertext.length - 16);
|
|
287
|
+
const encryptedData = ciphertext.subarray(0, ciphertext.length - 16);
|
|
288
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", Buffer.from(this.getWepayConfig().secretKey, "utf8"), Buffer.from(resource.nonce, "utf8"));
|
|
289
|
+
decipher.setAuthTag(authTag);
|
|
290
|
+
decipher.setAAD(Buffer.from(resource.associated_data ?? "", "utf8"));
|
|
291
|
+
return Buffer.concat([decipher.update(encryptedData), decipher.final()]).toString("utf8");
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* 发起微信支付 APIv3 请求。
|
|
295
|
+
*
|
|
296
|
+
* 根据 URL 计算请求路径,按 `方法\nURL\n时间戳\n随机串\n请求体\n`
|
|
297
|
+
* 构造签名并生成 `Authorization` 请求头;GET 请求体为空字符串,
|
|
298
|
+
* POST 请求体为 `data` 的 JSON 字符串。
|
|
299
|
+
*
|
|
300
|
+
* @param url 完整请求地址。
|
|
301
|
+
* @param data 请求体,缺省表示 GET 请求。
|
|
302
|
+
* @returns 响应数据。
|
|
303
|
+
*/
|
|
304
|
+
async wepayRequest(url, data) {
|
|
305
|
+
const wepay = this.getWepayConfig();
|
|
306
|
+
const path = url.replace(/^https?:\/\/[^/]+/, "");
|
|
307
|
+
const method = data == null ? "GET" : "POST";
|
|
308
|
+
const body = method === "POST" ? JSON.stringify(data) : "";
|
|
309
|
+
const params = {
|
|
310
|
+
method,
|
|
311
|
+
url: path,
|
|
312
|
+
time: String(Math.round(Date.now() / 1e3)),
|
|
313
|
+
rand: this.generateNonceStr(),
|
|
314
|
+
body,
|
|
315
|
+
signature: ""
|
|
316
|
+
};
|
|
317
|
+
params.signature = this.wepaySign([
|
|
318
|
+
params.method,
|
|
319
|
+
params.url,
|
|
320
|
+
params.time,
|
|
321
|
+
params.rand,
|
|
322
|
+
params.body
|
|
323
|
+
]);
|
|
324
|
+
const Authorization = [
|
|
325
|
+
`WECHATPAY2-SHA256-RSA2048 `,
|
|
326
|
+
`mchid="${wepay.merchantId}",`,
|
|
327
|
+
`nonce_str="${params.rand}",`,
|
|
328
|
+
`signature="${params.signature}",`,
|
|
329
|
+
`timestamp="${params.time}",`,
|
|
330
|
+
`serial_no="${wepay.certificateSerialNumber}"`
|
|
331
|
+
].join("");
|
|
332
|
+
const { data: res } = method === "POST" ? await xr.postJson({
|
|
333
|
+
url,
|
|
334
|
+
headers: { Authorization },
|
|
335
|
+
data
|
|
336
|
+
}) : await xr.getJson({
|
|
337
|
+
url,
|
|
338
|
+
headers: { Authorization }
|
|
339
|
+
});
|
|
340
|
+
return res;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
export { XPay };
|
package/lib/puppeteer.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { XClipboard } from "./clipboard.mjs";
|
|
2
|
+
import { XDispose } from "./dispose.mjs";
|
|
2
3
|
import { xr } from "@jayfong/x-request";
|
|
3
4
|
import { asyncMemoize, isFunction, isRegExp } from "vtils";
|
|
4
|
-
import { asyncExitHook } from "exit-hook";
|
|
5
5
|
import puppeteer from "puppeteer";
|
|
6
6
|
// src/puppeteer.ts
|
|
7
7
|
var XPuppeteer = class XPuppeteer {
|
|
@@ -19,7 +19,7 @@ var XPuppeteer = class XPuppeteer {
|
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
21
|
this.browser = browser;
|
|
22
|
-
|
|
22
|
+
XDispose.exit.add(() => this.disconnect());
|
|
23
23
|
return browser;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
@@ -36,7 +36,7 @@ var XPuppeteer = class XPuppeteer {
|
|
|
36
36
|
args: options.proxy ? [`--proxy-server=${options.proxy}`] : void 0
|
|
37
37
|
});
|
|
38
38
|
this.browser = browser;
|
|
39
|
-
|
|
39
|
+
XDispose.exit.add(() => this.close());
|
|
40
40
|
return browser;
|
|
41
41
|
}
|
|
42
42
|
/**
|
package/lib/random.d.mts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 随机 ID 与随机字符串生成工具。
|
|
3
|
+
*
|
|
4
|
+
* 提供碰撞概率极低的全局唯一 ID(CUID2)以及基于指定字符集的随机
|
|
5
|
+
* 字符串生成能力(字母数字、纯字母、纯数字)。所有方法均为静态方法,
|
|
6
|
+
* 可直接通过 `XRandom.xxx()` 调用。
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* XRandom.cuid2() // 'dj8fyuqt57ejnnbsnik8q1d6'
|
|
10
|
+
* XRandom.generateAlphanumeric(8) // 'a3Fk9Zx2'
|
|
11
|
+
* XRandom.generateAlphabetic(8) // 'AbCdEfGh'
|
|
12
|
+
* XRandom.generateNumeric(6) // '386251'
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* - `cuid2()` 生成的 ID 长度固定为 24 位,仅包含小写字母与数字,
|
|
16
|
+
* 适用于数据库主键、文件名等需要高唯一性的场景;
|
|
17
|
+
* - 随机字符串基于 `randomstring` 生成,各方法的字符集见对应说明;
|
|
18
|
+
* - 随机字符串不保证密码学安全,请勿用于令牌、密钥等安全敏感场景。
|
|
19
|
+
*/
|
|
20
|
+
export declare class XRandom {
|
|
21
|
+
/**
|
|
22
|
+
* 生成 24 位 CUID2 全局唯一 ID。
|
|
23
|
+
*
|
|
24
|
+
* CUID2 是 CUID 的改进版本,专为水平扩展与防碰撞设计,默认由
|
|
25
|
+
* 24 位小写字母与数字组成,无需配置即可直接使用。
|
|
26
|
+
*
|
|
27
|
+
* @returns 24 位小写字母与数字组成的唯一 ID 字符串。
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* XRandom.cuid2() // 'dj8fyuqt57ejnnbsnik8q1d6'
|
|
31
|
+
*
|
|
32
|
+
* @see https://github.com/paralleldrive/cuid2
|
|
33
|
+
*/
|
|
34
|
+
static cuid2(): string;
|
|
35
|
+
/**
|
|
36
|
+
* 生成由大小写字母与数字组成的随机字符串。
|
|
37
|
+
*
|
|
38
|
+
* 字符集为 `[0-9 a-z A-Z]`,共 62 种字符。
|
|
39
|
+
*
|
|
40
|
+
* @param length 生成字符串的长度,需为不小于 0 的整数,传 0 时返回空字符串。
|
|
41
|
+
* @returns 指定长度的字母数字随机字符串。
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* XRandom.generateAlphanumeric(8) // 'a3Fk9Zx2'
|
|
45
|
+
*/
|
|
46
|
+
static generateAlphanumeric(length: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* 生成由大小写英文字母组成的随机字符串。
|
|
49
|
+
*
|
|
50
|
+
* 字符集为 `[a-z A-Z]`,不包含数字。
|
|
51
|
+
*
|
|
52
|
+
* @param length 生成字符串的长度,需为不小于 0 的整数,传 0 时返回空字符串。
|
|
53
|
+
* @returns 指定长度的字母随机字符串。
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* XRandom.generateAlphabetic(8) // 'AbCdEfGh'
|
|
57
|
+
*/
|
|
58
|
+
static generateAlphabetic(length: number): string;
|
|
59
|
+
/**
|
|
60
|
+
* 生成由数字组成的随机字符串。
|
|
61
|
+
*
|
|
62
|
+
* 字符集为 `[0-9]`,首位允许为 0(例如 `'086251'`)。
|
|
63
|
+
*
|
|
64
|
+
* @param length 生成字符串的长度,需为不小于 0 的整数,传 0 时返回空字符串。
|
|
65
|
+
* @returns 指定长度的数字随机字符串。
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* XRandom.generateNumeric(6) // '386251'
|
|
69
|
+
*/
|
|
70
|
+
static generateNumeric(length: number): string;
|
|
71
|
+
}
|
package/lib/random.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createId } from "@paralleldrive/cuid2";
|
|
2
|
+
import * as randomstring from "randomstring";
|
|
3
|
+
// src/random.ts
|
|
4
|
+
/**
|
|
5
|
+
* 随机 ID 与随机字符串生成工具。
|
|
6
|
+
*
|
|
7
|
+
* 提供碰撞概率极低的全局唯一 ID(CUID2)以及基于指定字符集的随机
|
|
8
|
+
* 字符串生成能力(字母数字、纯字母、纯数字)。所有方法均为静态方法,
|
|
9
|
+
* 可直接通过 `XRandom.xxx()` 调用。
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* XRandom.cuid2() // 'dj8fyuqt57ejnnbsnik8q1d6'
|
|
13
|
+
* XRandom.generateAlphanumeric(8) // 'a3Fk9Zx2'
|
|
14
|
+
* XRandom.generateAlphabetic(8) // 'AbCdEfGh'
|
|
15
|
+
* XRandom.generateNumeric(6) // '386251'
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* - `cuid2()` 生成的 ID 长度固定为 24 位,仅包含小写字母与数字,
|
|
19
|
+
* 适用于数据库主键、文件名等需要高唯一性的场景;
|
|
20
|
+
* - 随机字符串基于 `randomstring` 生成,各方法的字符集见对应说明;
|
|
21
|
+
* - 随机字符串不保证密码学安全,请勿用于令牌、密钥等安全敏感场景。
|
|
22
|
+
*/
|
|
23
|
+
var XRandom = class {
|
|
24
|
+
/**
|
|
25
|
+
* 生成 24 位 CUID2 全局唯一 ID。
|
|
26
|
+
*
|
|
27
|
+
* CUID2 是 CUID 的改进版本,专为水平扩展与防碰撞设计,默认由
|
|
28
|
+
* 24 位小写字母与数字组成,无需配置即可直接使用。
|
|
29
|
+
*
|
|
30
|
+
* @returns 24 位小写字母与数字组成的唯一 ID 字符串。
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* XRandom.cuid2() // 'dj8fyuqt57ejnnbsnik8q1d6'
|
|
34
|
+
*
|
|
35
|
+
* @see https://github.com/paralleldrive/cuid2
|
|
36
|
+
*/
|
|
37
|
+
static cuid2() {
|
|
38
|
+
return createId();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 生成由大小写字母与数字组成的随机字符串。
|
|
42
|
+
*
|
|
43
|
+
* 字符集为 `[0-9 a-z A-Z]`,共 62 种字符。
|
|
44
|
+
*
|
|
45
|
+
* @param length 生成字符串的长度,需为不小于 0 的整数,传 0 时返回空字符串。
|
|
46
|
+
* @returns 指定长度的字母数字随机字符串。
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* XRandom.generateAlphanumeric(8) // 'a3Fk9Zx2'
|
|
50
|
+
*/
|
|
51
|
+
static generateAlphanumeric(length) {
|
|
52
|
+
return randomstring.generate({
|
|
53
|
+
charset: "alphanumeric",
|
|
54
|
+
length
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 生成由大小写英文字母组成的随机字符串。
|
|
59
|
+
*
|
|
60
|
+
* 字符集为 `[a-z A-Z]`,不包含数字。
|
|
61
|
+
*
|
|
62
|
+
* @param length 生成字符串的长度,需为不小于 0 的整数,传 0 时返回空字符串。
|
|
63
|
+
* @returns 指定长度的字母随机字符串。
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* XRandom.generateAlphabetic(8) // 'AbCdEfGh'
|
|
67
|
+
*/
|
|
68
|
+
static generateAlphabetic(length) {
|
|
69
|
+
return randomstring.generate({
|
|
70
|
+
charset: "alphabetic",
|
|
71
|
+
length
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 生成由数字组成的随机字符串。
|
|
76
|
+
*
|
|
77
|
+
* 字符集为 `[0-9]`,首位允许为 0(例如 `'086251'`)。
|
|
78
|
+
*
|
|
79
|
+
* @param length 生成字符串的长度,需为不小于 0 的整数,传 0 时返回空字符串。
|
|
80
|
+
* @returns 指定长度的数字随机字符串。
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* XRandom.generateNumeric(6) // '386251'
|
|
84
|
+
*/
|
|
85
|
+
static generateNumeric(length) {
|
|
86
|
+
return randomstring.generate({
|
|
87
|
+
charset: "numeric",
|
|
88
|
+
length
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
export { XRandom };
|
package/lib/redis.d.mts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Redis, RedisOptions } from "ioredis";
|
|
2
|
+
/**
|
|
3
|
+
* Redis 连接与行为配置项。
|
|
4
|
+
*
|
|
5
|
+
* 直接继承 ioredis 的 `RedisOptions`,作为 {@link XRedis} 构造时的入参类型。
|
|
6
|
+
* 保留为空继承的原因:
|
|
7
|
+
* - 调用方可继续使用 ioredis 原生的全部配置项(如 `host`、`port`、`db`、
|
|
8
|
+
* `password`、`keyPrefix` 等);
|
|
9
|
+
* - 后续若需追加项目级默认配置或扩展字段,只需在此声明,无需改动调用方。
|
|
10
|
+
*/
|
|
11
|
+
export interface XRedisOptions extends RedisOptions {}
|
|
12
|
+
/**
|
|
13
|
+
* Redis 客户端封装。
|
|
14
|
+
*
|
|
15
|
+
* 直接继承 ioredis 的 `Redis`,完整保留其命令、事件与类型定义,当前仅作为
|
|
16
|
+
* 统一入口存在,便于:
|
|
17
|
+
* - 集中管理连接实例的创建与复用;
|
|
18
|
+
* - 在需要时统一注入默认配置、日志或监控逻辑,而不影响调用方;
|
|
19
|
+
* - 后续按需覆写/扩展方法,同时保持对原有 API 的兼容。
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const redis = new XRedis({
|
|
23
|
+
* host: '127.0.0.1',
|
|
24
|
+
* port: 6379,
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* await redis.set('key', 'value');
|
|
28
|
+
* // 'OK'
|
|
29
|
+
* const value = await redis.get('key');
|
|
30
|
+
* // 'value'
|
|
31
|
+
*/
|
|
32
|
+
export declare class XRedis extends Redis {}
|
package/lib/redis.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Redis } from "ioredis";
|
|
2
|
+
// src/redis.ts
|
|
3
|
+
/**
|
|
4
|
+
* Redis 客户端封装。
|
|
5
|
+
*
|
|
6
|
+
* 直接继承 ioredis 的 `Redis`,完整保留其命令、事件与类型定义,当前仅作为
|
|
7
|
+
* 统一入口存在,便于:
|
|
8
|
+
* - 集中管理连接实例的创建与复用;
|
|
9
|
+
* - 在需要时统一注入默认配置、日志或监控逻辑,而不影响调用方;
|
|
10
|
+
* - 后续按需覆写/扩展方法,同时保持对原有 API 的兼容。
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const redis = new XRedis({
|
|
14
|
+
* host: '127.0.0.1',
|
|
15
|
+
* port: 6379,
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* await redis.set('key', 'value');
|
|
19
|
+
* // 'OK'
|
|
20
|
+
* const value = await redis.get('key');
|
|
21
|
+
* // 'value'
|
|
22
|
+
*/
|
|
23
|
+
var XRedis = class extends Redis {};
|
|
24
|
+
export { XRedis };
|
package/lib/runner.d.mts
CHANGED
|
@@ -1,52 +1,80 @@
|
|
|
1
|
-
import { Answers, DistinctQuestion } from "inquirer";
|
|
2
1
|
import { MsValue } from "vtils";
|
|
3
|
-
|
|
4
|
-
* `XRunner` 交互问题定义。
|
|
5
|
-
*
|
|
6
|
-
* 为了让配置回填逻辑可安全工作,要求问题必须包含 `name` 与 `message`。
|
|
7
|
-
*/
|
|
8
|
-
export interface XRunnerQuestion<TConfig extends Answers> extends Omit<DistinctQuestion<TConfig>, 'name' | 'message'> {
|
|
9
|
-
/** 问题对应的配置字段名。 */
|
|
10
|
-
name: Extract<keyof TConfig, string>;
|
|
11
|
-
/** 展示给用户的问题文案。 */
|
|
12
|
-
message: string;
|
|
13
|
-
}
|
|
2
|
+
import { AsyncOrSync } from "vtils/types";
|
|
14
3
|
/**
|
|
15
4
|
* `XRunner` 的运行配置。
|
|
16
5
|
*/
|
|
17
|
-
export interface XRunnerOptions<TConfig extends
|
|
18
|
-
/**
|
|
6
|
+
export interface XRunnerOptions<TConfig extends Record<string, any>> {
|
|
7
|
+
/**
|
|
8
|
+
* 交互配置持久化文件路径(JSON)。
|
|
9
|
+
*
|
|
10
|
+
* 启动时若文件不存在会自动创建;读取到的历史配置会传给 `provideConfig`,
|
|
11
|
+
* 收集到的新配置最终以 2 空格缩进写回该文件,供下次运行复用。
|
|
12
|
+
*/
|
|
19
13
|
configFile: string;
|
|
20
14
|
/**
|
|
21
15
|
* 失败重试间隔。
|
|
22
16
|
*
|
|
23
17
|
* - 固定值:每次失败都等待同一个时长;
|
|
24
18
|
* - 函数:可按重试次数动态计算,`retryCount` 从 `1` 开始计数。
|
|
19
|
+
*
|
|
20
|
+
* 时长取值与 `vtils.ms` 一致:数字表示毫秒,字符串需带单位
|
|
21
|
+
* (如 `'500ms'`、`'1s'`);值非法时 `ms` 会抛错并终止重试循环。
|
|
25
22
|
*/
|
|
26
23
|
delay: MsValue | ((retryCount: number) => MsValue);
|
|
27
|
-
/** inquirer 交互问题列表。 */
|
|
28
|
-
questions: Array<XRunnerQuestion<TConfig>>;
|
|
29
24
|
/**
|
|
30
|
-
*
|
|
25
|
+
* 收集本次运行配置的交互函数。
|
|
26
|
+
*
|
|
27
|
+
* 入参 `prevConfig` 为启动时读取到的历史配置(文件缺失或内容非法时兜底为 `{}`),
|
|
28
|
+
* 通常用于回填交互默认值;返回最终配置,支持同步或异步返回。
|
|
29
|
+
*/
|
|
30
|
+
provideConfig: (prevConfig: Partial<TConfig>) => AsyncOrSync<TConfig>;
|
|
31
|
+
/**
|
|
32
|
+
* 核心执行函数,接收 `provideConfig` 收集到的最终配置。
|
|
31
33
|
*
|
|
32
|
-
*
|
|
34
|
+
* - 成功(resolve):结束运行,`run()` 正常返回;
|
|
35
|
+
* - 失败(reject):进入重试分支,按 `delay` 等待后再次调用。
|
|
33
36
|
*/
|
|
34
37
|
run: (config: TConfig) => Promise<void> | void;
|
|
35
38
|
}
|
|
36
39
|
/**
|
|
37
40
|
* 带配置持久化与失败重试能力的交互式运行器。
|
|
38
41
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* 典型用法:CLI 启动时先询问用户配置并落盘复用,随后循环执行业务函数,
|
|
43
|
+
* 失败时自动等待并重试。
|
|
44
|
+
*
|
|
45
|
+
* 运行流程(详见 {@link XRunner.run}):
|
|
46
|
+
* 1. 确保配置文件存在(缺失时自动创建);
|
|
47
|
+
* 2. 读取历史配置,作为本次提问默认值;
|
|
48
|
+
* 3. 用户确认后回写配置文件,保证下次可复用;
|
|
49
|
+
* 4. 执行业务函数,成功即退出;
|
|
50
|
+
* 5. 失败时按 `delay` 等待后重试,直到成功或出现不可恢复错误。
|
|
44
51
|
*/
|
|
45
|
-
export declare class XRunner<TConfig extends
|
|
52
|
+
export declare class XRunner<TConfig extends Record<string, any>> {
|
|
46
53
|
private options;
|
|
54
|
+
/**
|
|
55
|
+
* 创建运行器实例。
|
|
56
|
+
*
|
|
57
|
+
* @param options 运行配置,各字段语义见 {@link XRunnerOptions}。
|
|
58
|
+
*/
|
|
47
59
|
constructor(options: XRunnerOptions<TConfig>);
|
|
48
60
|
/**
|
|
49
61
|
* 启动运行器。
|
|
62
|
+
*
|
|
63
|
+
* 依次执行:确保配置文件存在 → 读取历史配置 → 通过 `provideConfig`
|
|
64
|
+
* 收集本次配置 → 写回配置文件 → 循环执行 `run`。
|
|
65
|
+
*
|
|
66
|
+
* 边界与异常说明:
|
|
67
|
+
* - 配置文件缺失时由 `XFs.ensureFile` 自动补齐目录并创建空文件;
|
|
68
|
+
* - 配置内容非法(非 JSON)时 `readJson(..., { throws: false })` 返回
|
|
69
|
+
* `null`,经 `|| {}` 兜底为空对象,不阻断启动;
|
|
70
|
+
* - `provideConfig` 抛错或写回配置失败时,错误直接向外抛出,不进入重试;
|
|
71
|
+
* - `run` 抛错时进入重试分支:`retryCount` 自增后计算等待时长,打印
|
|
72
|
+
* 错误与重试提示,等待后重新执行;
|
|
73
|
+
* - `delay` 为函数时按 `retryCount`(从 `1` 开始)动态计算;其返回值
|
|
74
|
+
* 若无法被 `ms` 解析,`ms` 会抛错并终止重试循环。
|
|
75
|
+
*
|
|
76
|
+
* @returns 业务函数成功后 resolve 为 `undefined`;仅在成功或抛出
|
|
77
|
+
* 不可恢复错误时返回,否则持续重试。
|
|
50
78
|
*/
|
|
51
79
|
run(): Promise<void>;
|
|
52
80
|
}
|