hx-cdn-forge 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +803 -0
- package/dist/index.css +504 -0
- package/dist/index.d.mts +328 -0
- package/dist/index.d.ts +328 -0
- package/dist/index.js +1405 -0
- package/dist/index.mjs +1380 -0
- package/dist/split.js +309 -0
- package/package.json +89 -0
- package/src/cli/split.ts +311 -0
- package/src/core/__tests__/cdnNodes.test.ts +142 -0
- package/src/core/__tests__/manifest.test.ts +117 -0
- package/src/core/cdnNodes.ts +251 -0
- package/src/core/chunkedFetcher.ts +529 -0
- package/src/core/config.ts +66 -0
- package/src/core/fetcher.ts +476 -0
- package/src/core/manifest.ts +174 -0
- package/src/index.ts +73 -0
- package/src/react/CDNContext.tsx +268 -0
- package/src/react/CDNNodeSelector/index.tsx +258 -0
- package/src/react/CDNNodeSelector/styles.css +578 -0
- package/src/types.ts +316 -0
package/README.md
ADDED
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# HX-CDN-Forge
|
|
4
|
+
|
|
5
|
+
**GitHub 文件 CDN 代理 + 大文件差分切片 + 多 CDN 并行下载**
|
|
6
|
+
|
|
7
|
+
一次 `reqByCDN()` 调用,自动处理一切
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/hx-cdn-forge)
|
|
10
|
+
[](https://www.typescriptlang.org/)
|
|
11
|
+
[](https://reactjs.org/)
|
|
12
|
+
[](LICENSE)
|
|
13
|
+
|
|
14
|
+
[功能特性](#功能特性) • [快速开始](#快速开始) • [CLI 切片工具](#cli-切片工具) • [API 文档](#api-文档) • [Tag 版本管理](#tag-版本管理)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 功能特性
|
|
21
|
+
|
|
22
|
+
HX-CDN-Forge v2 专注于 **GitHub 文件的 CDN 代理加速**,提供对使用者完全透明的大文件支持。
|
|
23
|
+
|
|
24
|
+
### 🚀 核心能力
|
|
25
|
+
|
|
26
|
+
- **⚡ 透明请求** — `await reqByCDN("path")` 自动检测文件是否已切片,透明下载并拼接
|
|
27
|
+
- **✂️ 大文件差分切片** — CLI 工具将 >20MB 的文件切片,生成 `info.yaml` 清单 + `.cache.yaml` 增量缓存
|
|
28
|
+
- **🔥 多 CDN 并行下载** — 不同分片分配给不同 CDN 节点,动态负载均衡 + 任务窃取
|
|
29
|
+
- **🚀 极速模式 (Turbo)** — 同一分片从多个 CDN 同时请求,`Promise.any()` 取最快响应
|
|
30
|
+
- **🏷️ Tag 版本管理** — 通过 `bot-{commitId}-{timestamp}` tag 避免 jsDelivr 分支缓存失效
|
|
31
|
+
- **⚡ 实时延迟测速** — 自动测试所有 CDN 节点,选择最快的
|
|
32
|
+
- **💾 持久化存储** — 用户的节点选择自动保存到 localStorage
|
|
33
|
+
- **🛡️ TypeScript** — 完整类型定义
|
|
34
|
+
- **⚛️ React + 纯 JS** — `ForgeEngine` 可独立使用,也提供 React Context/Hooks
|
|
35
|
+
|
|
36
|
+
### 📦 内置 CDN 节点
|
|
37
|
+
|
|
38
|
+
| CDN 节点 | 地区 | 单文件限制 | 说明 |
|
|
39
|
+
|----------|------|-----------|------|
|
|
40
|
+
| **jsDelivr (Main)** | 全球 | 20 MB | jsDelivr 主节点 |
|
|
41
|
+
| **jsDelivr (Fastly)** | 全球 | 20 MB | Fastly CDN 加速 |
|
|
42
|
+
| **jsDelivr (Testing)** | 全球 | 20 MB | 测试节点 |
|
|
43
|
+
| **JSD Mirror** | 中国 | 20 MB | 腾讯云 EdgeOne 加速镜像 |
|
|
44
|
+
| **Zstatic** | 中国 | 20 MB | Zstatic CDN 镜像 |
|
|
45
|
+
| **GitHub Raw** | 全球 | 100 MB | GitHub 原始文件服务 |
|
|
46
|
+
| **Cloudflare Worker** | 全球 | 无限制 | 自定义 Worker 代理 |
|
|
47
|
+
|
|
48
|
+
> 💡 内置 6 个预设节点 + 支持自定义 Cloudflare Worker 代理节点。大于 20MB 的文件请使用 CLI 切片工具预处理。
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 快速开始
|
|
53
|
+
|
|
54
|
+
### 安装
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install hx-cdn-forge
|
|
58
|
+
# 或
|
|
59
|
+
pnpm add hx-cdn-forge
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 基础使用 (React)
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { CDNProvider, useCDNUrl, useReqByCDN, createForgeConfig } from 'hx-cdn-forge';
|
|
66
|
+
import 'hx-cdn-forge/styles.css';
|
|
67
|
+
|
|
68
|
+
// 推荐使用 tag 避免 jsDelivr 缓存问题
|
|
69
|
+
const config = createForgeConfig({
|
|
70
|
+
user: 'HengXin666',
|
|
71
|
+
repo: 'my-assets',
|
|
72
|
+
ref: 'bot-a1b2c3-20260329',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
function App() {
|
|
76
|
+
return (
|
|
77
|
+
<CDNProvider config={config}>
|
|
78
|
+
<MyContent />
|
|
79
|
+
</CDNProvider>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function MyContent() {
|
|
84
|
+
// 小文件: 直接获取 URL
|
|
85
|
+
const imgUrl = useCDNUrl('screenshots/demo.png');
|
|
86
|
+
|
|
87
|
+
// 大文件 / 任意文件: 透明请求
|
|
88
|
+
const reqByCDN = useReqByCDN();
|
|
89
|
+
|
|
90
|
+
const handleLoad = async () => {
|
|
91
|
+
const result = await reqByCDN('static/ass/loli.ass', (p) => {
|
|
92
|
+
console.log(`${p.percentage}% | ${(p.speed / 1024 / 1024).toFixed(1)} MB/s`);
|
|
93
|
+
});
|
|
94
|
+
// result.blob — 完整文件 (无论原文件是否切片)
|
|
95
|
+
const text = await result.blob.text();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div>
|
|
100
|
+
<img src={imgUrl} alt="demo" />
|
|
101
|
+
<button onClick={handleLoad}>加载大文件</button>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 基础使用 (纯 JS / Node)
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { ForgeEngine, createForgeConfig } from 'hx-cdn-forge';
|
|
111
|
+
|
|
112
|
+
const config = createForgeConfig(
|
|
113
|
+
{ user: 'HengXin666', repo: 'my-assets', ref: 'bot-a1b2c3-20260329' },
|
|
114
|
+
{
|
|
115
|
+
splitStoragePath: 'static/cdn-black',
|
|
116
|
+
mappingPrefix: 'static',
|
|
117
|
+
turboMode: true, // 开启极速模式
|
|
118
|
+
turboConcurrentCDNs: 3, // 每个分片同时请求 3 个 CDN
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const engine = new ForgeEngine(config);
|
|
123
|
+
await engine.initialize();
|
|
124
|
+
|
|
125
|
+
// 透明请求 — 自动检测切片
|
|
126
|
+
const result = await engine.reqByCDN('static/ass/loli.ass', (p) => {
|
|
127
|
+
console.log(`${p.percentage}% | ETA: ${p.eta.toFixed(1)}s`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
console.log(`下载完成: ${result.totalSize} bytes, 耗时 ${result.totalTime.toFixed(0)}ms`);
|
|
131
|
+
console.log(`使用切片模式: ${result.usedSplitMode}`);
|
|
132
|
+
console.log(`使用并行模式: ${result.usedParallelMode}`);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## CLI 切片工具
|
|
138
|
+
|
|
139
|
+
对于超过 CDN 节点单文件限制 (默认 20MB) 的大文件,需要先用 CLI 工具进行切片。
|
|
140
|
+
|
|
141
|
+
### 安装 & 使用
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# 全局安装后直接使用
|
|
145
|
+
npm install -g hx-cdn-forge
|
|
146
|
+
hx-cdn-split --help
|
|
147
|
+
|
|
148
|
+
# 或通过 npx
|
|
149
|
+
npx hx-cdn-split --help
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 基本用法
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# 将 25MB 的 ASS 文件切片
|
|
156
|
+
hx-cdn-split \
|
|
157
|
+
--source static/ass/loli.ass \
|
|
158
|
+
--output static/cdn-black \
|
|
159
|
+
--prefix static
|
|
160
|
+
|
|
161
|
+
# 使用自定义切片大小
|
|
162
|
+
hx-cdn-split -s data/big.bin -o cdn-data -p data -c 10MB
|
|
163
|
+
|
|
164
|
+
# 强制重新生成 (忽略缓存)
|
|
165
|
+
hx-cdn-split -s static/ass/loli.ass -o static/cdn-black -p static -f
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 参数说明
|
|
169
|
+
|
|
170
|
+
| 参数 | 短写 | 必填 | 说明 |
|
|
171
|
+
|------|------|------|------|
|
|
172
|
+
| `--source` | `-s` | ✅ | 源文件路径 |
|
|
173
|
+
| `--output` | `-o` | ✅ | 输出存储根目录 |
|
|
174
|
+
| `--prefix` | `-p` | ❌ | 映射前缀 (从 source 路径去除) |
|
|
175
|
+
| `--chunk-size` | `-c` | ❌ | 切片大小,默认 `19MB`。支持 B/KB/MB/GB 后缀 |
|
|
176
|
+
| `--force` | `-f` | ❌ | 强制重新生成,忽略 `.cache.yaml` |
|
|
177
|
+
| `--help` | `-h` | — | 显示帮助 |
|
|
178
|
+
|
|
179
|
+
### 切片存储结构
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
仓库根目录/
|
|
183
|
+
├── static/
|
|
184
|
+
│ └── ass/
|
|
185
|
+
│ └── loli.ass ← 源文件 (25MB)
|
|
186
|
+
│
|
|
187
|
+
└── static/cdn-black/ ← splitStoragePath (配置的存储路径)
|
|
188
|
+
└── ass/
|
|
189
|
+
└── loli.ass/ ← 映射目录 (去除了 "static" 前缀)
|
|
190
|
+
├── 0-loli.ass ← 切片 0 (19MB)
|
|
191
|
+
├── 1-loli.ass ← 切片 1 (6MB)
|
|
192
|
+
├── info.yaml ← 切片清单
|
|
193
|
+
└── .cache.yaml ← 源文件哈希 (增量更新检测)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### info.yaml 示例
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
originalName: loli.ass
|
|
200
|
+
totalSize: 26214400
|
|
201
|
+
mimeType: text/x-ssa
|
|
202
|
+
chunkSize: 19922944
|
|
203
|
+
createdAt: 2026-03-29T08:00:00.000Z
|
|
204
|
+
chunks:
|
|
205
|
+
- fileName: 0-loli.ass
|
|
206
|
+
index: 0
|
|
207
|
+
size: 19922944
|
|
208
|
+
sha256: a1b2c3...
|
|
209
|
+
- fileName: 1-loli.ass
|
|
210
|
+
index: 1
|
|
211
|
+
size: 6291456
|
|
212
|
+
sha256: d4e5f6...
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 增量更新
|
|
216
|
+
|
|
217
|
+
CLI 工具会在输出目录生成 `.cache.yaml`,记录源文件的路径和 SHA-256 哈希。再次运行时:
|
|
218
|
+
|
|
219
|
+
- **源文件未变化** → 自动跳过,输出 `⏭️ 源文件未变化,跳过`
|
|
220
|
+
- **源文件已变化** → 重新生成所有切片
|
|
221
|
+
- **使用 `--force`** → 无条件重新生成
|
|
222
|
+
|
|
223
|
+
推荐在 `package.json` 中添加脚本:
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"scripts": {
|
|
228
|
+
"cdn:split": "hx-cdn-split -s static/ass/loli.ass -o static/cdn-black -p static"
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 透明请求原理
|
|
236
|
+
|
|
237
|
+
`reqByCDN(filePath)` 的内部流程:
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
reqByCDN("static/ass/loli.ass")
|
|
241
|
+
│
|
|
242
|
+
▼
|
|
243
|
+
┌─────────────────────────┐
|
|
244
|
+
│ 1. 计算 info.yaml 路径 │
|
|
245
|
+
│ splitStoragePath + │
|
|
246
|
+
│ mapPath(filePath) │
|
|
247
|
+
│ + "/info.yaml" │
|
|
248
|
+
└────────────┬────────────┘
|
|
249
|
+
│
|
|
250
|
+
▼
|
|
251
|
+
┌───────────────┐
|
|
252
|
+
│ 2. 请求 CDN │
|
|
253
|
+
│ 获取 info.yaml│
|
|
254
|
+
└───────┬───────┘
|
|
255
|
+
│
|
|
256
|
+
┌──────┴──────┐
|
|
257
|
+
│ │
|
|
258
|
+
200 OK 404
|
|
259
|
+
│ │
|
|
260
|
+
▼ ▼
|
|
261
|
+
┌───────────┐ ┌──────────────┐
|
|
262
|
+
│ 3A. 解析 │ │ 3B. 直接下载 │
|
|
263
|
+
│ info.yaml │ │ 原始文件 │
|
|
264
|
+
│ 并行下载 │ │ (单文件模式) │
|
|
265
|
+
│ 各切片 │ └──────────────┘
|
|
266
|
+
│ → 拼接 │
|
|
267
|
+
└───────────┘
|
|
268
|
+
│
|
|
269
|
+
▼
|
|
270
|
+
DownloadResult
|
|
271
|
+
{ blob, totalSize, usedSplitMode, ... }
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**对调用者完全透明** — 无论文件是否切片,API 调用方式完全相同。
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 多 CDN 并行下载
|
|
279
|
+
|
|
280
|
+
### 标准模式
|
|
281
|
+
|
|
282
|
+
不同分片分配给不同 CDN 节点,充分利用多路带宽:
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
文件: big.bin (38MB, 切片为 2 块)
|
|
286
|
+
|
|
287
|
+
切片 0 (19MB) ─── jsDelivr Main ────→ ███████████ 完成 1.2s
|
|
288
|
+
切片 1 (19MB) ─── JSD Mirror ────→ ███████████ 完成 0.9s
|
|
289
|
+
↓
|
|
290
|
+
Blob 拼接 → 完整文件
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
特性:
|
|
294
|
+
- **EWMA 速度估计** — 实时追踪各节点下载速度
|
|
295
|
+
- **动态负载均衡** — 更多任务分配给更快的节点
|
|
296
|
+
- **任务窃取** — 快速节点完成后自动接管慢速节点的待执行任务
|
|
297
|
+
|
|
298
|
+
### 极速模式 (Turbo Mode)
|
|
299
|
+
|
|
300
|
+
同一分片同时从多个 CDN 请求,`Promise.any()` 取最快响应:
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
切片 0 (19MB):
|
|
304
|
+
├── jsDelivr Main ────→ ███████ 最先完成 ✅ → 采用
|
|
305
|
+
├── JSD Mirror ────→ █████████ (abort)
|
|
306
|
+
└── Zstatic ────→ ████████████ (abort)
|
|
307
|
+
|
|
308
|
+
切片 1 (19MB):
|
|
309
|
+
├── jsDelivr Main ────→ ██████████ (abort)
|
|
310
|
+
├── JSD Mirror ────→ ██████ 最先完成 ✅ → 采用
|
|
311
|
+
└── Zstatic ────→ █████████████ (abort)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**牺牲带宽换取最低延迟**,适合对加载速度有极致要求的场景。
|
|
315
|
+
|
|
316
|
+
开启极速模式:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
const config = createForgeConfig(
|
|
320
|
+
{ user: '...', repo: '...', ref: '...' },
|
|
321
|
+
{
|
|
322
|
+
splitStoragePath: 'static/cdn-black',
|
|
323
|
+
mappingPrefix: 'static',
|
|
324
|
+
turboMode: true, // 开启极速模式
|
|
325
|
+
turboConcurrentCDNs: 3, // 每个分片同时请求 3 个 CDN
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Tag 版本管理
|
|
333
|
+
|
|
334
|
+
### 问题
|
|
335
|
+
|
|
336
|
+
jsDelivr 对分支 (如 `main`) 的缓存时间很长。当文件内容更新后,CDN 可能长时间返回旧数据。
|
|
337
|
+
|
|
338
|
+
使用 commit hash 作为 ref 需要 **两次 git 提交**:
|
|
339
|
+
1. 第一次提交数据 (才能获取 commit hash)
|
|
340
|
+
2. 第二次提交 hash 到配置中
|
|
341
|
+
|
|
342
|
+
### 解决方案
|
|
343
|
+
|
|
344
|
+
使用 `bot-{shortCommitId}-{timestamp}` 格式的 tag,配合 GitHub Actions 自动管理:
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
推送到 main 分支
|
|
348
|
+
│
|
|
349
|
+
▼
|
|
350
|
+
GitHub Actions
|
|
351
|
+
│
|
|
352
|
+
├── 创建 tag: bot-a1b2c3-20260329160000
|
|
353
|
+
│
|
|
354
|
+
└── 保留最新 2 个 tag,删除更旧的
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**只需一次 git 提交**,流水线自动创建带有当前 commit hash 的 tag。
|
|
358
|
+
|
|
359
|
+
### 配置 GitHub Actions
|
|
360
|
+
|
|
361
|
+
在仓库中添加 `.github/workflows/cdn-tag.yml`:
|
|
362
|
+
|
|
363
|
+
```yaml
|
|
364
|
+
name: CDN Tag Manager
|
|
365
|
+
|
|
366
|
+
on:
|
|
367
|
+
push:
|
|
368
|
+
branches: [main]
|
|
369
|
+
|
|
370
|
+
permissions:
|
|
371
|
+
contents: write
|
|
372
|
+
|
|
373
|
+
jobs:
|
|
374
|
+
create-cdn-tag:
|
|
375
|
+
runs-on: ubuntu-latest
|
|
376
|
+
steps:
|
|
377
|
+
- uses: actions/checkout@v4
|
|
378
|
+
with:
|
|
379
|
+
fetch-depth: 0
|
|
380
|
+
|
|
381
|
+
- name: Create and manage CDN tags
|
|
382
|
+
run: |
|
|
383
|
+
SHORT_SHA=$(git rev-parse --short HEAD)
|
|
384
|
+
TIMESTAMP=$(date +%Y%m%d%H%M%S)
|
|
385
|
+
NEW_TAG="bot-${SHORT_SHA}-${TIMESTAMP}"
|
|
386
|
+
|
|
387
|
+
# 创建新 tag
|
|
388
|
+
git tag "${NEW_TAG}"
|
|
389
|
+
git push origin "${NEW_TAG}"
|
|
390
|
+
|
|
391
|
+
# 只保留最新 2 个 bot- tag
|
|
392
|
+
BOT_TAGS=$(git tag -l 'bot-*' --sort=-creatordate)
|
|
393
|
+
TAG_COUNT=$(echo "${BOT_TAGS}" | grep -c '^bot-' || true)
|
|
394
|
+
|
|
395
|
+
if [ "${TAG_COUNT}" -gt 2 ]; then
|
|
396
|
+
echo "${BOT_TAGS}" | tail -n +3 | while IFS= read -r tag; do
|
|
397
|
+
[ -n "${tag}" ] && git push origin --delete "${tag}" 2>/dev/null || true
|
|
398
|
+
[ -n "${tag}" ] && git tag -d "${tag}" 2>/dev/null || true
|
|
399
|
+
done
|
|
400
|
+
fi
|
|
401
|
+
|
|
402
|
+
echo "✅ CDN tag: ${NEW_TAG}"
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### 使用 tag
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
const config = createForgeConfig({
|
|
409
|
+
user: 'HengXin666',
|
|
410
|
+
repo: 'my-assets',
|
|
411
|
+
ref: 'bot-a1b2c3-20260329160000', // 使用 tag 而非分支名
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## API 文档
|
|
418
|
+
|
|
419
|
+
### `createForgeConfig(github, options?)`
|
|
420
|
+
|
|
421
|
+
创建配置对象。
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
import { createForgeConfig } from 'hx-cdn-forge';
|
|
425
|
+
|
|
426
|
+
const config = createForgeConfig(
|
|
427
|
+
// GitHub 仓库信息 (必填)
|
|
428
|
+
{
|
|
429
|
+
user: 'HengXin666',
|
|
430
|
+
repo: 'my-assets',
|
|
431
|
+
ref: 'bot-a1b2c3-20260329', // branch / tag / commit hash
|
|
432
|
+
},
|
|
433
|
+
// 可选配置
|
|
434
|
+
{
|
|
435
|
+
// --- 切片相关 ---
|
|
436
|
+
splitStoragePath: 'static/cdn-black', // 切片存储根路径
|
|
437
|
+
mappingPrefix: 'static', // 路径映射前缀
|
|
438
|
+
splitThreshold: 20 * 1024 * 1024, // 切片阈值 (默认 20MB)
|
|
439
|
+
|
|
440
|
+
// --- 节点相关 ---
|
|
441
|
+
nodes: undefined, // 自定义节点列表 (默认使用内置 6 个)
|
|
442
|
+
defaultNodeId: undefined, // 默认节点 ID (默认自动测速选择)
|
|
443
|
+
autoTest: true, // 初始化时自动测速
|
|
444
|
+
|
|
445
|
+
// --- 测速 ---
|
|
446
|
+
testTimeout: 5000, // 测速超时 (ms)
|
|
447
|
+
testRetries: 2, // 测速重试次数
|
|
448
|
+
|
|
449
|
+
// --- 并行下载 ---
|
|
450
|
+
maxConcurrency: 6, // 最大并发数
|
|
451
|
+
chunkTimeout: 30000, // 单分片超时 (ms)
|
|
452
|
+
maxRetries: 3, // 单分片重试次数
|
|
453
|
+
enableWorkStealing: true, // 任务窃取
|
|
454
|
+
|
|
455
|
+
// --- 极速模式 ---
|
|
456
|
+
turboMode: false, // 是否开启极速模式
|
|
457
|
+
turboConcurrentCDNs: 3, // 极速模式下同时请求的 CDN 数量
|
|
458
|
+
|
|
459
|
+
// --- 持久化 ---
|
|
460
|
+
storageKey: 'hx-cdn-forge-node', // localStorage 键名
|
|
461
|
+
},
|
|
462
|
+
);
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### `ForgeEngine`
|
|
466
|
+
|
|
467
|
+
核心引擎类,独立于 React,可在任何 JS 环境使用。
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
import { ForgeEngine } from 'hx-cdn-forge';
|
|
471
|
+
|
|
472
|
+
const engine = new ForgeEngine(config);
|
|
473
|
+
|
|
474
|
+
// 初始化 (自动测速 + 选择最快节点)
|
|
475
|
+
await engine.initialize();
|
|
476
|
+
|
|
477
|
+
// 透明请求
|
|
478
|
+
const result = await engine.reqByCDN('static/ass/loli.ass', onProgress);
|
|
479
|
+
|
|
480
|
+
// URL 构建 (小文件)
|
|
481
|
+
const url = engine.buildUrl('screenshots/demo.png');
|
|
482
|
+
|
|
483
|
+
// 节点管理
|
|
484
|
+
engine.getNodes(); // 获取所有节点
|
|
485
|
+
engine.getCurrentNode(); // 获取当前节点
|
|
486
|
+
engine.selectNode('jsd-mirror'); // 手动选择节点
|
|
487
|
+
engine.getSortedNodes(); // 按延迟排序的节点
|
|
488
|
+
|
|
489
|
+
// 测速
|
|
490
|
+
await engine.testAllNodes();
|
|
491
|
+
await engine.testAndSelectBest();
|
|
492
|
+
await engine.testAllNodesStreaming((result) => { /* 流式回调 */ });
|
|
493
|
+
|
|
494
|
+
// 其他
|
|
495
|
+
engine.getConfig(); // 获取规范化后的配置
|
|
496
|
+
engine.isInitialized(); // 是否已初始化
|
|
497
|
+
engine.clearSplitInfoCache(); // 清除 info.yaml 缓存
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### `DownloadResult`
|
|
501
|
+
|
|
502
|
+
`reqByCDN()` 返回的结果对象:
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
interface DownloadResult {
|
|
506
|
+
blob: Blob; // 完整文件数据
|
|
507
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
508
|
+
totalSize: number; // 文件大小 (字节)
|
|
509
|
+
totalTime: number; // 耗时 (毫秒)
|
|
510
|
+
contentType: string; // MIME 类型
|
|
511
|
+
usedSplitMode: boolean; // 是否使用了切片下载
|
|
512
|
+
usedParallelMode: boolean; // 是否使用了并行模式
|
|
513
|
+
nodeContributions: Map<string, {
|
|
514
|
+
bytes: number;
|
|
515
|
+
chunks: number;
|
|
516
|
+
avgSpeed: number;
|
|
517
|
+
}>;
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### `DownloadProgress`
|
|
522
|
+
|
|
523
|
+
进度回调参数:
|
|
524
|
+
|
|
525
|
+
```ts
|
|
526
|
+
interface DownloadProgress {
|
|
527
|
+
loaded: number; // 已下载字节
|
|
528
|
+
total: number; // 总字节
|
|
529
|
+
percentage: number; // 百分比 (0-100)
|
|
530
|
+
speed: number; // 当前速度 (字节/秒)
|
|
531
|
+
eta: number; // 预估剩余时间 (秒)
|
|
532
|
+
completedChunks: number; // 已完成分片数
|
|
533
|
+
totalChunks: number; // 总分片数
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### CDN 节点预设 & 工具
|
|
538
|
+
|
|
539
|
+
```ts
|
|
540
|
+
import {
|
|
541
|
+
CDN_NODE_PRESETS,
|
|
542
|
+
DEFAULT_GITHUB_CDN_NODES,
|
|
543
|
+
createWorkerNode,
|
|
544
|
+
CDNTester,
|
|
545
|
+
getSortedNodesWithLatency,
|
|
546
|
+
} from 'hx-cdn-forge';
|
|
547
|
+
|
|
548
|
+
// 预设节点
|
|
549
|
+
CDN_NODE_PRESETS.jsdelivr_main // jsDelivr 主节点
|
|
550
|
+
CDN_NODE_PRESETS.jsdelivr_fastly // jsDelivr Fastly
|
|
551
|
+
CDN_NODE_PRESETS.jsdelivr_testing // jsDelivr Testing
|
|
552
|
+
CDN_NODE_PRESETS.jsd_mirror // JSD Mirror (中国)
|
|
553
|
+
CDN_NODE_PRESETS.zstatic // Zstatic (中国)
|
|
554
|
+
CDN_NODE_PRESETS.github_raw // GitHub Raw
|
|
555
|
+
|
|
556
|
+
// 默认节点列表 (以上 6 个)
|
|
557
|
+
DEFAULT_GITHUB_CDN_NODES
|
|
558
|
+
|
|
559
|
+
// 创建 Cloudflare Worker 代理节点
|
|
560
|
+
const workerNode = createWorkerNode('your-worker.workers.dev');
|
|
561
|
+
|
|
562
|
+
// 独立测速工具
|
|
563
|
+
const tester = new CDNTester(5000, 2);
|
|
564
|
+
const results = await tester.testAll(nodes);
|
|
565
|
+
const bestId = tester.getBestNodeId(results);
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Manifest 工具
|
|
569
|
+
|
|
570
|
+
解析和序列化 `info.yaml` / `.cache.yaml` 的轻量级工具 (无外部依赖):
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
import {
|
|
574
|
+
parseInfoYaml,
|
|
575
|
+
serializeInfoYaml,
|
|
576
|
+
parseCacheYaml,
|
|
577
|
+
serializeCacheYaml,
|
|
578
|
+
} from 'hx-cdn-forge';
|
|
579
|
+
|
|
580
|
+
const info = parseInfoYaml(yamlText); // string → SplitInfo
|
|
581
|
+
const yaml = serializeInfoYaml(info); // SplitInfo → string
|
|
582
|
+
|
|
583
|
+
const cache = parseCacheYaml(yamlText); // string → SplitCache
|
|
584
|
+
const cYaml = serializeCacheYaml(cache); // SplitCache → string
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## React API
|
|
590
|
+
|
|
591
|
+
### `<CDNProvider>`
|
|
592
|
+
|
|
593
|
+
```tsx
|
|
594
|
+
import { CDNProvider, createForgeConfig } from 'hx-cdn-forge';
|
|
595
|
+
|
|
596
|
+
const config = createForgeConfig({
|
|
597
|
+
user: 'HengXin666',
|
|
598
|
+
repo: 'my-assets',
|
|
599
|
+
ref: 'bot-a1b2c3-20260329',
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
<CDNProvider
|
|
603
|
+
config={config}
|
|
604
|
+
onInitialized={(node) => console.log('就绪:', node?.name)}
|
|
605
|
+
onNodeChange={(node) => console.log('切换到:', node.name)}
|
|
606
|
+
>
|
|
607
|
+
<App />
|
|
608
|
+
</CDNProvider>
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### `useCDN()`
|
|
612
|
+
|
|
613
|
+
获取完整的 CDN Context:
|
|
614
|
+
|
|
615
|
+
```tsx
|
|
616
|
+
const {
|
|
617
|
+
config, // ForgeConfig
|
|
618
|
+
currentNode, // CDNNode | null
|
|
619
|
+
nodes, // CDNNodeWithLatency[]
|
|
620
|
+
isTesting, // boolean
|
|
621
|
+
isInitialized, // boolean
|
|
622
|
+
latencyResults, // Map<string, LatencyResult>
|
|
623
|
+
selectNode, // (nodeId: string) => void
|
|
624
|
+
testAllNodes, // () => Promise<LatencyResult[]>
|
|
625
|
+
reqByCDN, // (path, onProgress?) => Promise<DownloadResult>
|
|
626
|
+
buildUrl, // (path) => string
|
|
627
|
+
getSortedNodes, // () => CDNNodeWithLatency[]
|
|
628
|
+
} = useCDN();
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### `useReqByCDN()`
|
|
632
|
+
|
|
633
|
+
获取透明请求函数:
|
|
634
|
+
|
|
635
|
+
```tsx
|
|
636
|
+
const reqByCDN = useReqByCDN();
|
|
637
|
+
|
|
638
|
+
const result = await reqByCDN('static/ass/loli.ass', (p) => {
|
|
639
|
+
console.log(`${p.percentage}% | ${(p.speed / 1024 / 1024).toFixed(1)} MB/s`);
|
|
640
|
+
});
|
|
641
|
+
// result.blob — 完整文件
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### `useCDNUrl(path)`
|
|
645
|
+
|
|
646
|
+
获取小文件的 CDN URL:
|
|
647
|
+
|
|
648
|
+
```tsx
|
|
649
|
+
const url = useCDNUrl('screenshots/demo.png');
|
|
650
|
+
// → "https://cdn.jsdelivr.net/gh/user/repo@ref/screenshots/demo.png"
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### `useCurrentCDNNode()`
|
|
654
|
+
|
|
655
|
+
获取当前选中的 CDN 节点:
|
|
656
|
+
|
|
657
|
+
```tsx
|
|
658
|
+
const node = useCurrentCDNNode();
|
|
659
|
+
console.log(node?.name); // "jsDelivr (Main)"
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### `useCDNStatus()`
|
|
663
|
+
|
|
664
|
+
`useCDN()` 的别名,获取完整状态。
|
|
665
|
+
|
|
666
|
+
### `<CDNNodeSelector>`
|
|
667
|
+
|
|
668
|
+
可视化节点选择器组件:
|
|
669
|
+
|
|
670
|
+
```tsx
|
|
671
|
+
import { CDNNodeSelector } from 'hx-cdn-forge';
|
|
672
|
+
import 'hx-cdn-forge/styles.css';
|
|
673
|
+
|
|
674
|
+
<CDNNodeSelector
|
|
675
|
+
showLatency={true}
|
|
676
|
+
showRegion={true}
|
|
677
|
+
showRefreshButton={true}
|
|
678
|
+
compact={false}
|
|
679
|
+
title="CDN 节点"
|
|
680
|
+
onChange={(node) => console.log('选择:', node.name)}
|
|
681
|
+
onTestComplete={(results) => console.log('测速完成:', results)}
|
|
682
|
+
/>
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
## 完整配置示例
|
|
688
|
+
|
|
689
|
+
### 基础配置 (自动测速)
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
const config = createForgeConfig({
|
|
693
|
+
user: 'HengXin666',
|
|
694
|
+
repo: 'my-assets',
|
|
695
|
+
ref: 'bot-a1b2c3-20260329',
|
|
696
|
+
});
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### 带切片的配置
|
|
700
|
+
|
|
701
|
+
```ts
|
|
702
|
+
const config = createForgeConfig(
|
|
703
|
+
{ user: 'HengXin666', repo: 'my-assets', ref: 'bot-a1b2c3-20260329' },
|
|
704
|
+
{
|
|
705
|
+
splitStoragePath: 'static/cdn-black',
|
|
706
|
+
mappingPrefix: 'static',
|
|
707
|
+
},
|
|
708
|
+
);
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### 极速模式 + 自定义节点
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
import { CDN_NODE_PRESETS, createWorkerNode, createForgeConfig } from 'hx-cdn-forge';
|
|
715
|
+
|
|
716
|
+
const config = createForgeConfig(
|
|
717
|
+
{ user: 'HengXin666', repo: 'my-assets', ref: 'bot-a1b2c3-20260329' },
|
|
718
|
+
{
|
|
719
|
+
splitStoragePath: 'static/cdn-black',
|
|
720
|
+
mappingPrefix: 'static',
|
|
721
|
+
turboMode: true,
|
|
722
|
+
turboConcurrentCDNs: 3,
|
|
723
|
+
nodes: [
|
|
724
|
+
CDN_NODE_PRESETS.jsdelivr_main,
|
|
725
|
+
CDN_NODE_PRESETS.jsdelivr_fastly,
|
|
726
|
+
CDN_NODE_PRESETS.jsd_mirror,
|
|
727
|
+
CDN_NODE_PRESETS.zstatic,
|
|
728
|
+
createWorkerNode('my-proxy.workers.dev'),
|
|
729
|
+
],
|
|
730
|
+
},
|
|
731
|
+
);
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## 项目结构
|
|
737
|
+
|
|
738
|
+
```
|
|
739
|
+
src/
|
|
740
|
+
├── types.ts # 统一类型定义
|
|
741
|
+
├── index.ts # 入口导出
|
|
742
|
+
├── core/
|
|
743
|
+
│ ├── config.ts # 配置默认值 + 工厂
|
|
744
|
+
│ ├── cdnNodes.ts # CDN 节点预设 + 测速工具
|
|
745
|
+
│ ├── manifest.ts # info.yaml / .cache.yaml 解析器
|
|
746
|
+
│ ├── chunkedFetcher.ts # 多 CDN 并行分块下载引擎
|
|
747
|
+
│ └── fetcher.ts # ForgeEngine 核心引擎
|
|
748
|
+
├── cli/
|
|
749
|
+
│ └── split.ts # CLI 切片工具 (hx-cdn-split)
|
|
750
|
+
└── react/
|
|
751
|
+
├── CDNContext.tsx # Provider + Hooks
|
|
752
|
+
└── CDNNodeSelector/
|
|
753
|
+
├── index.tsx # 节点选择器组件
|
|
754
|
+
└── styles.css # 组件样式
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
759
|
+
## 浏览器支持
|
|
760
|
+
|
|
761
|
+
- Chrome / Edge: 最新 2 个版本
|
|
762
|
+
- Firefox: 最新 2 个版本
|
|
763
|
+
- Safari: 最新 2 个版本
|
|
764
|
+
- 需要 `fetch`、`ReadableStream`、`Promise.any` 支持 (ES2021+)
|
|
765
|
+
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
## 开发
|
|
769
|
+
|
|
770
|
+
```bash
|
|
771
|
+
git clone https://github.com/HengXin666/HX-CDN-Forge.git
|
|
772
|
+
cd HX-CDN-Forge
|
|
773
|
+
npm install
|
|
774
|
+
|
|
775
|
+
npm run build # 构建
|
|
776
|
+
npm run dev # 启动开发服务器
|
|
777
|
+
npm run type-check # 类型检查
|
|
778
|
+
npm test # 运行测试
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
---
|
|
782
|
+
|
|
783
|
+
## License
|
|
784
|
+
|
|
785
|
+
MIT © HX
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
## 致谢
|
|
790
|
+
|
|
791
|
+
- [jsDelivr](https://www.jsdelivr.com/) — 开源 CDN 服务
|
|
792
|
+
- [JSD Mirror](https://cdn.jsdmirror.com/) — jsDelivr 中国镜像
|
|
793
|
+
- [Cloudflare Workers](https://workers.cloudflare.com/) — 无服务器平台
|
|
794
|
+
|
|
795
|
+
---
|
|
796
|
+
|
|
797
|
+
<div align="center">
|
|
798
|
+
|
|
799
|
+
**[⬆ 返回顶部](#hx-cdn-forge)**
|
|
800
|
+
|
|
801
|
+
Made with ❤️ by HX
|
|
802
|
+
|
|
803
|
+
</div>
|