@cdlab996/genid 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -244
- package/dist/index.cjs +85 -237
- package/dist/index.d.cts +71 -238
- package/dist/index.d.mts +71 -238
- package/dist/index.mjs +83 -235
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -1,73 +1,28 @@
|
|
|
1
1
|
# @cdlab996/genid
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@cdlab996/genid)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
基于 Snowflake 算法的高性能分布式唯一 ID 生成器,支持漂移算法和时钟回拨处理。
|
|
4
7
|
|
|
5
8
|
## 特性
|
|
6
9
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
10
|
+
- **漂移算法** - 高并发场景下突破每毫秒序列号上限,性能更优
|
|
11
|
+
- **时钟回拨处理** - 使用保留序列号优雅降级,不阻塞 ID 生成
|
|
12
|
+
- **灵活配置** - 支持自定义时间戳、节点 ID、序列号的位长度分配
|
|
13
|
+
- **ID 验证** - 支持严格/宽松模式校验 ID 有效性
|
|
14
|
+
- **运行监控** - 内置统计、解析和二进制格式化调试工具
|
|
12
15
|
|
|
13
|
-
##
|
|
16
|
+
## 安装
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
```bash
|
|
19
|
+
# npm
|
|
20
|
+
npm install @cdlab996/genid
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
A[开始生成 ID] --> B{是否处于漂移状态?}
|
|
20
|
-
|
|
21
|
-
B -->|否| C[正常路径]
|
|
22
|
-
B -->|是| D[漂移路径]
|
|
23
|
-
|
|
24
|
-
C --> E{检测时钟}
|
|
25
|
-
E -->|时钟回拨| F[使用保留序列号 0-4]
|
|
26
|
-
E -->|时间前进| G[重置序列号]
|
|
27
|
-
E -->|同一毫秒| H{序列号是否溢出?}
|
|
28
|
-
|
|
29
|
-
H -->|否| I[序列号+1 正常生成]
|
|
30
|
-
H -->|是| J[进入漂移状态 时间戳+1]
|
|
31
|
-
|
|
32
|
-
D --> K{检测时间}
|
|
33
|
-
K -->|时间追上| L[退出漂移 恢复正常]
|
|
34
|
-
K -->|超过最大漂移| M[等待下一毫秒 退出漂移]
|
|
35
|
-
K -->|继续漂移| N{序列号是否溢出?}
|
|
36
|
-
|
|
37
|
-
N -->|否| O[使用当前序列号]
|
|
38
|
-
N -->|是| P[时间戳+1 重置序列号]
|
|
39
|
-
|
|
40
|
-
F --> Q[计算 ID]
|
|
41
|
-
G --> Q
|
|
42
|
-
I --> Q
|
|
43
|
-
J --> Q
|
|
44
|
-
L --> Q
|
|
45
|
-
M --> Q
|
|
46
|
-
O --> Q
|
|
47
|
-
P --> Q
|
|
48
|
-
|
|
49
|
-
Q --> R[更新统计]
|
|
50
|
-
R --> S[返回 ID]
|
|
22
|
+
# pnpm
|
|
23
|
+
pnpm add @cdlab996/genid
|
|
51
24
|
```
|
|
52
25
|
|
|
53
|
-
### ID 结构(64-bit)
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
|------------ 时间戳 ------------|-- 工作节点 ID --|-- 序列号 --|
|
|
57
|
-
42-52 bits 1-15 bits 3-21 bits
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
**位分配示例(默认配置):**
|
|
61
|
-
|
|
62
|
-
- 时间戳:52 bits(可用约 139 年)
|
|
63
|
-
- 工作节点 ID:6 bits(支持 64 个节点)
|
|
64
|
-
- 序列号:6 bits(每毫秒 59 个 ID,5-63)
|
|
65
|
-
|
|
66
|
-
**序列号分配:**
|
|
67
|
-
|
|
68
|
-
- `0-4`:保留用于时钟回拨
|
|
69
|
-
- `5-maxSeqNumber`:正常使用
|
|
70
|
-
|
|
71
26
|
## 快速开始
|
|
72
27
|
|
|
73
28
|
```typescript
|
|
@@ -78,175 +33,97 @@ const genid = new GenidOptimized({ workerId: 1 })
|
|
|
78
33
|
|
|
79
34
|
// 生成 ID
|
|
80
35
|
const id = genid.nextId()
|
|
81
|
-
console.log(id) // 123456789012345
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## API 参考
|
|
85
|
-
|
|
86
|
-
### 构造函数
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
new GenidOptimized(options: GenidOptions)
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
**配置选项**
|
|
93
|
-
|
|
94
|
-
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
95
|
-
|------|------|------|--------|------|
|
|
96
|
-
| `workerId` | number | ✅ | - | 工作节点 ID(范围:0 到 2^workerIdBitLength-1) |
|
|
97
|
-
| `method` | GenidMethod | ❌ | `DRIFT` | 算法类型:`DRIFT` 或 `TRADITIONAL` |
|
|
98
|
-
| `baseTime` | number | ❌ | `1577836800000` | 起始时间戳(毫秒,默认:2020-01-01) |
|
|
99
|
-
| `workerIdBitLength` | number | ❌ | `6` | 工作节点 ID 位数(1-15) |
|
|
100
|
-
| `seqBitLength` | number | ❌ | `6` | 序列号位数(3-21) |
|
|
101
|
-
| `maxSeqNumber` | number | ❌ | `2^seqBitLength-1` | 最大序列号 |
|
|
102
|
-
| `minSeqNumber` | number | ❌ | `5` | 最小序列号(0-4 保留用于时钟回拨) |
|
|
103
|
-
| `topOverCostCount` | number | ❌ | `2000` | 最大漂移次数 |
|
|
104
|
-
|
|
105
|
-
### 生成 ID
|
|
106
|
-
|
|
107
|
-
#### `nextId()`
|
|
108
|
-
|
|
109
|
-
返回 Number 或 BigInt 类型的 ID(自动选择)
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
const id = genid.nextId()
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
#### `nextNumber()`
|
|
116
|
-
|
|
117
|
-
返回 Number 类型的 ID(超出安全范围会抛出错误)
|
|
118
36
|
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
#### `nextBigId()`
|
|
37
|
+
// 批量生成
|
|
38
|
+
const ids = genid.nextBatch(1000)
|
|
124
39
|
|
|
125
|
-
|
|
40
|
+
// 解析 ID
|
|
41
|
+
const info = genid.parse(id)
|
|
42
|
+
// => { timestamp: Date, timestampMs: 1609459200000, workerId: 1, sequence: 42 }
|
|
126
43
|
|
|
127
|
-
|
|
128
|
-
|
|
44
|
+
// 验证 ID
|
|
45
|
+
genid.isValid(id) // true
|
|
129
46
|
```
|
|
130
47
|
|
|
131
|
-
|
|
48
|
+
## API
|
|
132
49
|
|
|
133
|
-
|
|
50
|
+
### `new GenidOptimized(options)`
|
|
134
51
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
52
|
+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
53
|
+
| ------------------- | ------------- | :---: | ------------------ | --------------------------------------------- |
|
|
54
|
+
| `workerId` | `number` | Yes | - | 工作节点 ID(0 ~ 2^workerIdBitLength-1) |
|
|
55
|
+
| `method` | `GenidMethod` | | `DRIFT` | 算法:`DRIFT`(漂移)或 `TRADITIONAL`(传统) |
|
|
56
|
+
| `baseTime` | `number` | | `1577836800000` | 起始时间戳,毫秒(默认 2020-01-01) |
|
|
57
|
+
| `workerIdBitLength` | `number` | | `6` | 节点 ID 位数(1-15) |
|
|
58
|
+
| `seqBitLength` | `number` | | `6` | 序列号位数(3-21) |
|
|
59
|
+
| `maxSeqNumber` | `number` | | `2^seqBitLength-1` | 最大序列号 |
|
|
60
|
+
| `minSeqNumber` | `number` | | `5` | 最小序列号(0-4 保留用于时钟回拨) |
|
|
61
|
+
| `topOverCostCount` | `number` | | `2000` | 最大漂移次数 |
|
|
139
62
|
|
|
140
|
-
###
|
|
141
|
-
|
|
142
|
-
#### `parse(id)`
|
|
143
|
-
|
|
144
|
-
解析 ID,提取组成部分
|
|
63
|
+
### 生成 ID
|
|
145
64
|
|
|
146
65
|
```typescript
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
// workerId: 1, // 工作节点 ID
|
|
153
|
-
// sequence: 42 // 序列号
|
|
154
|
-
// }
|
|
66
|
+
genid.nextId() // 返回 number | bigint(自动选择)
|
|
67
|
+
genid.nextNumber() // 返回 number(超出安全整数范围抛错)
|
|
68
|
+
genid.nextBigId() // 返回 bigint
|
|
69
|
+
genid.nextBatch(100) // 批量生成 100 个 ID
|
|
70
|
+
genid.nextBatch(100, true) // 批量生成 100 个 BigInt ID
|
|
155
71
|
```
|
|
156
72
|
|
|
157
|
-
###
|
|
158
|
-
|
|
159
|
-
#### `isValid(id, strictWorkerId?)`
|
|
160
|
-
|
|
161
|
-
验证 ID 是否为有效的 Snowflake ID
|
|
162
|
-
|
|
163
|
-
**参数**
|
|
164
|
-
- `id` - 要验证的 ID(支持 Number、BigInt、String 类型)
|
|
165
|
-
- `strictWorkerId` - 可选,是否严格验证 workerId 必须匹配当前实例(默认:false)
|
|
166
|
-
|
|
167
|
-
**返回值**
|
|
168
|
-
- `boolean` - ID 是否有效
|
|
169
|
-
|
|
170
|
-
**验证规则**
|
|
171
|
-
- ✅ ID 为正数
|
|
172
|
-
- ✅ ID 在 64 位范围内
|
|
173
|
-
- ✅ 时间戳在合理范围内(>= baseTime,<= 当前时间 + 1秒容差)
|
|
174
|
-
- ✅ workerId 在有效范围内(0 到 2^workerIdBitLength-1)
|
|
175
|
-
- ✅ 序列号在有效范围内(0 到 2^seqBitLength-1)
|
|
176
|
-
- ✅ 严格模式下:workerId 必须匹配当前实例
|
|
73
|
+
### 解析与验证
|
|
177
74
|
|
|
178
75
|
```typescript
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// 宽松模式:验证 ID 格式是否有效
|
|
183
|
-
genid.isValid(id) // true
|
|
184
|
-
genid.isValid(12345) // false(无效的 ID)
|
|
185
|
-
genid.isValid(-1) // false(负数)
|
|
186
|
-
genid.isValid('invalid') // false(无效格式)
|
|
76
|
+
// 解析 ID 的组成部分
|
|
77
|
+
genid.parse(id)
|
|
78
|
+
// => { timestamp: Date, timestampMs: number, workerId: number, sequence: number }
|
|
187
79
|
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
80
|
+
// 宽松验证:检查 ID 格式是否有效
|
|
81
|
+
genid.isValid(id) // true
|
|
82
|
+
genid.isValid(12345) // false
|
|
83
|
+
genid.isValid('invalid') // false
|
|
191
84
|
|
|
192
|
-
|
|
193
|
-
genid.isValid(
|
|
194
|
-
genid.isValid(
|
|
85
|
+
// 严格验证:要求 workerId 匹配当前实例
|
|
86
|
+
genid.isValid(id, true) // true(本实例生成的 ID)
|
|
87
|
+
genid.isValid(otherId, true) // false(其他实例生成的 ID)
|
|
195
88
|
```
|
|
196
89
|
|
|
197
90
|
### 统计与配置
|
|
198
91
|
|
|
199
|
-
#### `getStats()`
|
|
200
|
-
|
|
201
|
-
获取生成器统计信息
|
|
202
|
-
|
|
203
92
|
```typescript
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
93
|
+
// 获取运行统计
|
|
94
|
+
genid.getStats()
|
|
95
|
+
// => {
|
|
96
|
+
// totalGenerated: 1000,
|
|
97
|
+
// overCostCount: 10,
|
|
98
|
+
// turnBackCount: 2,
|
|
99
|
+
// uptimeMs: 60000,
|
|
100
|
+
// avgPerSecond: 16,
|
|
101
|
+
// currentState: 'NORMAL' | 'OVER_COST'
|
|
212
102
|
// }
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
#### `getConfig()`
|
|
216
|
-
|
|
217
|
-
获取配置信息
|
|
218
103
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
// {
|
|
104
|
+
// 获取当前配置
|
|
105
|
+
genid.getConfig()
|
|
106
|
+
// => {
|
|
222
107
|
// method: 'DRIFT',
|
|
223
108
|
// workerId: 1,
|
|
224
109
|
// workerIdRange: '0-63',
|
|
225
110
|
// sequenceRange: '5-63',
|
|
111
|
+
// maxSequence: 63,
|
|
226
112
|
// idsPerMillisecond: 59,
|
|
227
113
|
// baseTime: Date,
|
|
228
114
|
// timestampBits: 52,
|
|
229
115
|
// workerIdBits: 6,
|
|
230
116
|
// sequenceBits: 6
|
|
231
117
|
// }
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
#### `resetStats()`
|
|
235
118
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
```typescript
|
|
119
|
+
// 重置统计
|
|
239
120
|
genid.resetStats()
|
|
240
121
|
```
|
|
241
122
|
|
|
242
|
-
###
|
|
243
|
-
|
|
244
|
-
#### `formatBinary(id)`
|
|
245
|
-
|
|
246
|
-
格式化 ID 为二进制字符串
|
|
123
|
+
### 调试
|
|
247
124
|
|
|
248
125
|
```typescript
|
|
249
|
-
|
|
126
|
+
genid.formatBinary(id)
|
|
250
127
|
// ID: 123456789012345
|
|
251
128
|
// Binary (64-bit):
|
|
252
129
|
// 0000000000011010... - 时间戳 (52 bits) = 2025-10-17T...
|
|
@@ -256,99 +133,119 @@ console.log(genid.formatBinary(id))
|
|
|
256
133
|
|
|
257
134
|
## 使用示例
|
|
258
135
|
|
|
259
|
-
###
|
|
136
|
+
### 自定义位分配
|
|
260
137
|
|
|
261
138
|
```typescript
|
|
262
|
-
|
|
263
|
-
workerId: 1
|
|
264
|
-
})
|
|
139
|
+
import { GenidOptimized, GenidMethod } from '@cdlab996/genid'
|
|
265
140
|
|
|
266
|
-
// 生成单个 ID
|
|
267
|
-
const id1 = genid.nextId()
|
|
268
|
-
|
|
269
|
-
// 批量生成
|
|
270
|
-
const ids = genid.nextBatch(1000)
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
### 自定义配置
|
|
274
|
-
|
|
275
|
-
```typescript
|
|
276
141
|
const genid = new GenidOptimized({
|
|
277
142
|
workerId: 1,
|
|
278
143
|
method: GenidMethod.TRADITIONAL,
|
|
279
144
|
baseTime: new Date('2024-01-01').valueOf(),
|
|
280
|
-
workerIdBitLength: 10,
|
|
281
|
-
seqBitLength: 12,
|
|
282
|
-
topOverCostCount: 5000
|
|
145
|
+
workerIdBitLength: 10, // 支持 1024 个节点
|
|
146
|
+
seqBitLength: 12, // 每毫秒 4096 个 ID
|
|
147
|
+
topOverCostCount: 5000,
|
|
283
148
|
})
|
|
284
149
|
```
|
|
285
150
|
|
|
286
|
-
###
|
|
151
|
+
### 验证外部 ID
|
|
287
152
|
|
|
288
153
|
```typescript
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
// 生成并验证 ID
|
|
292
|
-
const id = genid.nextId()
|
|
293
|
-
if (genid.isValid(id)) {
|
|
294
|
-
console.log('ID 有效')
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// 验证外部 ID(例如从数据库或 API 获取的 ID)
|
|
154
|
+
// 验证从数据库或 API 获取的 ID
|
|
298
155
|
const externalId = '123456789012345'
|
|
299
156
|
if (genid.isValid(externalId)) {
|
|
300
157
|
const info = genid.parse(externalId)
|
|
301
|
-
console.log('
|
|
158
|
+
console.log('生成时间:', info.timestamp)
|
|
159
|
+
console.log('来自节点:', info.workerId)
|
|
302
160
|
} else {
|
|
303
|
-
console.error('ID
|
|
161
|
+
console.error('无效 ID')
|
|
304
162
|
}
|
|
305
|
-
|
|
306
|
-
// 严格验证(只接受当前实例生成的 ID)
|
|
307
|
-
const isMyId = genid.isValid(id, true)
|
|
308
163
|
```
|
|
309
164
|
|
|
310
|
-
###
|
|
165
|
+
### 性能监控
|
|
311
166
|
|
|
312
167
|
```typescript
|
|
313
|
-
// 定期检查统计信息
|
|
314
168
|
setInterval(() => {
|
|
315
169
|
const stats = genid.getStats()
|
|
316
|
-
console.log(
|
|
317
|
-
console.log(`漂移次数: ${stats.overCostCount}`)
|
|
170
|
+
console.log(`速率: ${stats.avgPerSecond} ID/s | 漂移: ${stats.overCostCount} | 回拨: ${stats.turnBackCount}`)
|
|
318
171
|
}, 10000)
|
|
319
172
|
```
|
|
320
173
|
|
|
321
174
|
## 算法模式
|
|
322
175
|
|
|
323
|
-
|
|
176
|
+
| 模式 | 说明 | 适用场景 |
|
|
177
|
+
| ----------------- | ---------------------------------------- | -------------------- |
|
|
178
|
+
| **DRIFT**(默认) | 序列号耗尽时借用未来时间戳,避免等待 | 高频 ID 生成、高并发 |
|
|
179
|
+
| **TRADITIONAL** | 严格按时间戳递增,序列号耗尽等待下一毫秒 | 对时间顺序严格要求 |
|
|
324
180
|
|
|
325
|
-
|
|
326
|
-
- 允许时间戳漂移以避免等待
|
|
327
|
-
- 适合高频 ID 生成场景
|
|
181
|
+
## 架构
|
|
328
182
|
|
|
329
|
-
###
|
|
183
|
+
### ID 结构(64-bit)
|
|
330
184
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
-
|
|
185
|
+
```
|
|
186
|
+
|------------ 时间戳 ------------|-- 工作节点 ID --|-- 序列号 --|
|
|
187
|
+
42-52 bits 1-15 bits 3-21 bits
|
|
188
|
+
```
|
|
334
189
|
|
|
335
|
-
|
|
190
|
+
默认配置:时间戳 52 bits(约 139 年)| 节点 ID 6 bits(64 个节点)| 序列号 6 bits(每毫秒 59 个 ID)
|
|
336
191
|
|
|
337
|
-
|
|
192
|
+
序列号 `0-4` 保留用于时钟回拨,正常使用从 `5` 开始。
|
|
193
|
+
|
|
194
|
+
### 核心流程
|
|
195
|
+
|
|
196
|
+
```mermaid
|
|
197
|
+
graph TB
|
|
198
|
+
A[开始生成 ID] --> B{是否处于漂移状态?}
|
|
199
|
+
|
|
200
|
+
B -->|否| C[正常路径]
|
|
201
|
+
B -->|是| D[漂移路径]
|
|
202
|
+
|
|
203
|
+
C --> E{检测时钟}
|
|
204
|
+
E -->|时钟回拨| F[使用保留序列号 0-4]
|
|
205
|
+
E -->|时间前进| G[重置序列号]
|
|
206
|
+
E -->|同一毫秒| H{序列号是否溢出?}
|
|
207
|
+
|
|
208
|
+
H -->|否| I[序列号+1 正常生成]
|
|
209
|
+
H -->|是| J[进入漂移状态 时间戳+1]
|
|
210
|
+
|
|
211
|
+
D --> K{检测时间}
|
|
212
|
+
K -->|时间追上| L[退出漂移 恢复正常]
|
|
213
|
+
K -->|超过最大漂移| M[等待下一毫秒 退出漂移]
|
|
214
|
+
K -->|继续漂移| N{序列号是否溢出?}
|
|
215
|
+
|
|
216
|
+
N -->|否| O[使用当前序列号]
|
|
217
|
+
N -->|是| P[时间戳+1 重置序列号]
|
|
218
|
+
|
|
219
|
+
F --> Q[计算 ID]
|
|
220
|
+
G --> Q
|
|
221
|
+
I --> Q
|
|
222
|
+
J --> Q
|
|
223
|
+
L --> Q
|
|
224
|
+
M --> Q
|
|
225
|
+
O --> Q
|
|
226
|
+
P --> Q
|
|
227
|
+
|
|
228
|
+
Q --> R[更新统计]
|
|
229
|
+
R --> S[返回 ID]
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## 注意事项
|
|
338
233
|
|
|
339
234
|
- 每个 Worker/进程必须使用**不同的 workerId**
|
|
340
|
-
-
|
|
235
|
+
- 实例**非线程安全**,不要跨线程共享
|
|
341
236
|
- `workerIdBitLength + seqBitLength` 不能超过 22
|
|
342
237
|
- 序列号 0-4 保留用于时钟回拨处理
|
|
343
|
-
- JavaScript
|
|
238
|
+
- 超出 JavaScript 安全整数范围(2^53-1)时,使用 `nextBigId()` 或 `nextId()`(自动返回 BigInt)
|
|
344
239
|
|
|
345
|
-
##
|
|
240
|
+
## 性能
|
|
346
241
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
242
|
+
| 指标 | 数值 |
|
|
243
|
+
| -------------------------- | ------------- |
|
|
244
|
+
| 单实例吞吐量 | > 50,000 ID/s |
|
|
245
|
+
| 每毫秒生成量(默认配置) | 59 个 |
|
|
246
|
+
| 最大节点数(默认配置) | 64 个 |
|
|
247
|
+
| 时间戳可用时长(默认配置) | ~139 年 |
|
|
351
248
|
|
|
352
|
-
##
|
|
249
|
+
## License
|
|
353
250
|
|
|
354
251
|
[MIT](./LICENSE) License © 2025-PRESENT [wudi](https://github.com/WuChenDi)
|