feat(seed): 支持从远程URL加载seed.txt并添加认证配置

新增环境变量配置支持从远程Gitea等URL加载seed.txt文件,包括:
- SEED_SOURCE: 指定来源(file/url)
- SEED_URL: 远程文件地址
- SEED_AUTH_HEADER/SEED_TOKEN: 认证配置
添加远程加载失败时自动回退到本地文件的功能
更新README文档说明配置方法
This commit is contained in:
2025-12-13 18:43:05 +08:00
parent e12e722300
commit 4554d4fade
4 changed files with 151 additions and 14 deletions

17
.env.example Normal file
View File

@@ -0,0 +1,17 @@
# 服务器端口
PORT=3000
# Seed 来源file 或 url
SEED_SOURCE=file
# 当 SEED_SOURCE=url 时配置远程原始TXT地址
# 示例Gitea Raw 文件直链或 API Raw 接口
# SEED_URL=https://gitea.example.com/<owner>/<repo>/raw/seed.txt
SEED_URL=
# 认证(任选其一)
# 1) 自定义 Authorization 头(例如 Bearer 或 Basic
# SEED_AUTH_HEADER=Bearer xxxxx
SEED_AUTH_HEADER=
# 2) Token 形式(将自动使用 Authorization: token <TOKEN>
SEED_TOKEN=

View File

@@ -166,6 +166,20 @@
- `npm install && npm start`
- 建议使用 systemd/pm2 守护进程,并在 Nginx 反向代理到 `127.0.0.1:<PORT>`
## 环境变量示例(.env.example
- 复制示例文件并按需修改:
- Windows PowerShell`Copy-Item .env.example .env`
- Linux/Mac`cp .env.example .env`
- 可用变量:
- `PORT`:服务器端口(默认 `3000`
- `SEED_SOURCE``file` 或 `url`(默认 `file`
- `SEED_URL`:当 `SEED_SOURCE=url` 时的远程TXT地址Gitea Raw直链或API
- `SEED_AUTH_HEADER`:自定义认证头(如 `Bearer <token>`
- `SEED_TOKEN`:令牌(将自动以 `Authorization: token <TOKEN>` 发送)
- 生效方式:修改 `.env` 后重启服务
- 相关代码位置:`server.js:19`(环境变量读取)、`server.js:419`(远程/本地加载逻辑)
## 常见问题FAQ
- 修改 `seed.txt` 是否需要重启?不需要,`GET /api/seed` 会重新读取。
@@ -190,6 +204,7 @@
- 页面视觉细节:为 `header` 与 `main` 增加间距≥30px背景设置 `background-attachment: fixed` 并覆盖视窗(居中、等比、无重复)
- 修复CSS 依赖路径对 `..` 上级目录的正确解析与落盘(兼容 Font Awesome 的 `../webfonts`);增强 CDN 回退匹配支持 `@scope` 包名jsDelivr / unpkg
- 新增:开源许可文件 `LICENSE`GPL-3.0),并在 README 增加许可说明与仓库地址
- 新增:`seed.txt` 支持远程来源Gitea 原始文件),通过环境变量配置
## 开源许可与仓库地址
@@ -200,4 +215,18 @@
- 分发时需保留版权声明与本许可证文本,并开放源代码
- 不提供任何形式的担保,详见 `LICENSE` 的免责声明章节
## Seed 配置(本地与远程)
- 默认行为:`GET /api/seed` 读取仓库根目录的本地 `seed.txt`
- 远程来源:将环境变量设置为以下值以从 Gitea 原始文件拉取
- `SEED_SOURCE=url`
- `SEED_URL=<你的Gitea原始TXT地址>`(例如:`https://gitea.example.com/api/v1/repos/<owner>/<repo>/raw/seed.txt?ref=main` 或 Raw 文件直链)
- 可选认证:
- `SEED_AUTH_HEADER="Bearer <token>"`(自定义 Authorization 头)
- 或 `SEED_TOKEN="<token>"`(自动使用 `Authorization: token <token>` 头)
- 回退机制:远程拉取失败时,若本地 `seed.txt` 存在则自动使用本地文件
- 相关代码:
- 远程/本地加载逻辑:`server.js:419``loadSeedTxt`
- 执行批量抓取:`server.js:440``runSeed`
Copyright © 2025 Asset Cache Server Developer By [SnowZ](https://ckk.photo8.site/Photo8/Asset-cache)

View File

@@ -1,18 +1,83 @@
# 示例本文件内置一些常用的CSS/JS地址用于测试
# 注释行以 # 开头;空行将被忽略
# Bootstrap 5.3.0 样式与脚本
# Bootstrap
https://cdn.jsdmirror.com/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css
https://cdn.jsdmirror.com/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js
https://cdn.tailwindcss.com
# Element UI 2.15.13 样式(用于验证字体依赖自动抓取)
https://dist.jicelue.com/css/npm/element-ui@2.15.13/lib/theme-chalk/index.css
# Font Awesome Free 6验证 webfonts 自动抓取)
# font-awesome
https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.5.1/css/all.min.css
https://s4.zstatic.net/ajax/libs/font-awesome/5.15.4/css/all.min.css
https://s4.zstatic.net/ajax/libs/font-awesome/5.15.4/css/v4-shims.min.css
https://s4.zstatic.net/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css
# layer
https://s4.zstatic.net/ajax/libs/layer/3.1.1/layer.js
https://s4.zstatic.net/ajax/libs/layer/3.1.1/theme/default/layer.css
# iconify
https://code.iconify.design/3/3.1.1/iconify.min.js
# JQUERY
https://upcdn.b0.upaiyun.com/libs/jquery/jquery-1.9.1.min.js
https://upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.3.min.js
https://upcdn.b0.upaiyun.com/libs/jqueryui/jquery.ui-1.9.1.min.js
https://cdn.staticfile.net/jquery/3.7.1/jquery.js
https://cdn.staticfile.net/jquery/3.7.1/jquery.min.js
https://cdn.staticfile.net/jquery/3.7.1/jquery.min.map
https://cdn.staticfile.net/jquery/3.7.1/jquery.slim.js
https://cdn.staticfile.net/jquery/3.7.1/jquery.slim.min.js
https://cdn.staticfile.net/jquery/3.7.1/jquery.slim.min.map
# xgplayer
https://unpkg.byted-static.com/xgplayer/3.0.10/dist/index.min.js
https://unpkg.byted-static.com/xgplayer-hls/3.0.10/dist/index.min.js
https://unpkg.byted-static.com/xgplayer-flv/3.0.10/dist/index.min.js
https://unpkg.byted-static.com/xgplayer-dash/3.0.0-alpha.2/dist/index.min.js
https://unpkg.byted-static.com/xgplayer-mp4/3.0.10/dist/index.min.js
https://unpkg.byted-static.com/xgplayer/3.0.10/dist/index.min.css
https://cdn.jsdelivr.net/npm/xgplayer-hls.js@3.0.20/dist/index.min.js
# swarmcloud
https://cdn.jsdelivr.net/npm/@swarmcloud/hls/p2p-engine.min.js
https://cdn.jsdelivr.net/npm/@swarmcloud/hls/hls.min.js
# tailwindcss/browser
https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.1.17/dist/index.global.min.js
https://cdn.tailwindcss.com
# iconify
https://code.iconify.design/3/3.1.1/iconify.min.js
# notyf
https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.css
https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.js
# Vue.js
https://unpkg.com/vue@3.5.7/dist/vue.global.prod.js
https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js
# Axios
https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js
https://cdn.jsdelivr.net/npm/axios@0.26.0/dist/axios.min.js
https://cdn.jsdmirror.com/npm/axios@0.21.1/dist/axios.min.js
# Swiper
https://cdnjs.cloudflare.com/ajax/libs/Swiper/7.4.1/swiper-bundle.min.js
https://cdnjs.cloudflare.com/ajax/libs/Swiper/7.4.1/swiper-bundle.min.css
# npm
https://cdn.jsdelivr.net/npm/element-ui@2.15.7/lib/theme-chalk/index.css
https://cdn.jsdelivr.net/npm/element-ui@2.15.7/lib/index.js
https://cdn.jsdelivr.net/npm/element-ui/lib/index.js
https://cdn.jsdelivr.net/npm/element-ui/lib/theme-chalk/index.css
https://cdn.jsdmirror.com/npm/element-ui@2.15.13/lib/theme-chalk/index.css
https://cdn.jsdmirror.com/npm/element-ui@2.15.13/lib/index.js
https://cdn.jsdmirror.com/npm/element-ui@2.15.6/lib/theme-chalk/index.css
https://cdn.jsdmirror.com/npm/element-ui@2.15.6/lib/index.js
# Emoji
https://cdn.staticfile.net/emoji-js/3.8.0/emoji.js
https://cdn.staticfile.net/emoji-js/3.8.0/emoji.css

View File

@@ -16,6 +16,11 @@ const CSS_DIR = path.join(CACHE_ROOT, 'css')
const JS_DIR = path.join(CACHE_ROOT, 'js')
const SEED_FILE = path.join(__dirname, 'seed.txt')
const PUBLIC_DIR = path.join(__dirname, 'public')
// Seed 源设置(默认读取本地 seed.txt可选从远程URL读取原始TXT
const SEED_SOURCE = (process.env.SEED_SOURCE || 'file').toLowerCase()
const SEED_URL = process.env.SEED_URL || ''
const SEED_TOKEN = process.env.SEED_TOKEN || ''
const SEED_AUTH_HEADER = process.env.SEED_AUTH_HEADER || ''
// 中间件
app.use(express.json({ limit: '2mb' }))
@@ -405,14 +410,35 @@ app.get('/health', (req, res) => {
// 已移除外部提交接口:/api/upload-txt 与 /api/cache
/**
* 从项目内置 seed.txt 加载URL并执行批量缓存
* 加载 Seed 文本(本地或远程)
* - 当 SEED_SOURCE = 'url' 且配置了 SEED_URL 时,优先从远程拉取
* - 支持可选认证头SEED_AUTH_HEADER或 SEED_TOKEN 以 Authorization: token <TOKEN> 形式
* - 远程拉取失败时回退到本地文件(若存在)
* @returns {Promise<string>} 返回原始TXT文本可能为空字符串
*/
async function loadSeedTxt() {
if (SEED_SOURCE === 'url' && SEED_URL) {
try {
const headers = { 'User-Agent': 'AssetCache/1.0', 'Accept': 'text/plain, */*' }
if (SEED_AUTH_HEADER) headers['Authorization'] = SEED_AUTH_HEADER
else if (SEED_TOKEN) headers['Authorization'] = `token ${SEED_TOKEN}`
const resp = await axios.get(SEED_URL, { responseType: 'text', timeout: 20000, headers })
const txt = typeof resp.data === 'string' ? resp.data : (resp.data?.toString?.() || '')
if (txt && txt.trim().length > 0) return txt
} catch (e) {
// 回退到本地
}
}
if (fs.existsSync(SEED_FILE)) return fs.readFileSync(SEED_FILE, 'utf8')
return ''
}
/**
* 加载Seed本地/远程)并执行批量缓存
* @returns {Promise<{count:number, results:any[]}>}
*/
async function runSeedFromFile() {
if (!fs.existsSync(SEED_FILE)) {
return { count: 0, results: [] }
}
const txt = fs.readFileSync(SEED_FILE, 'utf8')
async function runSeed() {
const txt = await loadSeedTxt()
const urls = parseTxtToUrls(txt)
if (urls.length === 0) return { count: 0, results: [] }
const results = await batchFetch(urls)
@@ -422,7 +448,7 @@ async function runSeedFromFile() {
// 触发内置seed.txt抓取
app.get('/api/seed', async (req, res) => {
try {
const r = await runSeedFromFile()
const r = await runSeed()
const mapped = (r.results || []).map(x => ({
url: x.url,
saved: x.saved ? getPublicPath(x.url, x.type, x.saved) : '',