feat: PeopleLib 开放文献客户端,集成 Z-Library 与 LibGen 等多源检索
Electron 桌面客户端,聚合多个开放获取文献源的搜索、详情与下载。 新增数据源: - Z-Library:邮箱登录(凭据本地存储),会话失效自动重登 - LibGen:适配新版 libgen.ac 前端(旧版 search.php 镜像已全部下线) - Memory of the World、Sci-Hub、Anna's Archive 基础设施: - mirror.js:镜像故障转移,支持串行优先与并发竞速两种策略, 失效镜像 5 分钟冷却后自动重试,避免站点恢复后被永久跳过 - http.js:统一 15 秒请求超时,防止单个卡死镜像拖垮整次搜索 - settings.js:全局代理配置持久化,经 Electron net.fetch 生效于所有请求 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit
1a1288ce18
@@ -0,0 +1,114 @@
|
||||
// 通用镜像管理与故障转移
|
||||
// 每个 source 提供候选镜像列表,运行时记住"当前可用"镜像;
|
||||
// 失败的镜像会被临时拉黑(带过期时间),避免站点恢复后永远不再尝试。
|
||||
|
||||
const BAD_TTL = 5 * 60 * 1000; // 失败镜像的冷却时间
|
||||
|
||||
const stateCache = new Map(); // prefix -> { current, bad: Map<mirror, ts> }
|
||||
|
||||
function stateOf(prefix) {
|
||||
let c = stateCache.get(prefix);
|
||||
if (!c) {
|
||||
c = { current: null, bad: new Map() };
|
||||
stateCache.set(prefix, c);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
function isBad(c, mirror) {
|
||||
const ts = c.bad.get(mirror);
|
||||
if (!ts) return false;
|
||||
if (Date.now() - ts > BAD_TTL) {
|
||||
c.bad.delete(mirror);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function markBad(prefix, mirror) {
|
||||
const c = stateOf(prefix);
|
||||
c.bad.set(mirror, Date.now());
|
||||
if (c.current === mirror) c.current = null;
|
||||
}
|
||||
|
||||
function markGood(prefix, mirror) {
|
||||
const c = stateOf(prefix);
|
||||
c.current = mirror;
|
||||
c.bad.delete(mirror);
|
||||
}
|
||||
|
||||
function currentFor(prefix, mirrors) {
|
||||
const c = stateOf(prefix);
|
||||
if (c.current && mirrors.includes(c.current)) return c.current;
|
||||
return mirrors[0];
|
||||
}
|
||||
|
||||
// 候选顺序:上次成功的优先,其余按原顺序,已拉黑的排到最后兜底
|
||||
function candidates(prefix, mirrors) {
|
||||
const c = stateOf(prefix);
|
||||
const fresh = [];
|
||||
const stale = [];
|
||||
for (const m of mirrors) {
|
||||
if (m === c.current) continue;
|
||||
(isBad(c, m) ? stale : fresh).push(m);
|
||||
}
|
||||
const out = [];
|
||||
if (c.current && mirrors.includes(c.current)) out.push(c.current);
|
||||
out.push(...fresh, ...stale);
|
||||
return out.length ? out : mirrors.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* 串行尝试:按优先级逐个调用 fn(mirror),第一个成功即返回。
|
||||
* 适用于镜像少、需要严格优先级的场景。
|
||||
*/
|
||||
async function tryMirrors(prefix, mirrors, fn) {
|
||||
const list = candidates(prefix, mirrors);
|
||||
let lastErr;
|
||||
for (const m of list) {
|
||||
try {
|
||||
const r = await fn(m);
|
||||
markGood(prefix, m);
|
||||
return r;
|
||||
} catch (e) {
|
||||
lastErr = e;
|
||||
markBad(prefix, m);
|
||||
}
|
||||
}
|
||||
throw lastErr || new Error('所有镜像均不可用');
|
||||
}
|
||||
|
||||
/**
|
||||
* 竞速尝试:同时向所有候选镜像发起请求,最先成功的胜出。
|
||||
* 适用于镜像多且大量失效的场景(如 LibGen),避免串行等待累加。
|
||||
*/
|
||||
async function raceMirrors(prefix, mirrors, fn) {
|
||||
const list = candidates(prefix, mirrors);
|
||||
if (!list.length) throw new Error('没有可用镜像');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let pending = list.length;
|
||||
let settled = false;
|
||||
let lastErr;
|
||||
|
||||
for (const m of list) {
|
||||
Promise.resolve()
|
||||
.then(() => fn(m))
|
||||
.then((r) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
markGood(prefix, m);
|
||||
resolve(r);
|
||||
})
|
||||
.catch((e) => {
|
||||
lastErr = e;
|
||||
markBad(prefix, m);
|
||||
if (--pending === 0 && !settled) {
|
||||
reject(lastErr || new Error('所有镜像均不可用'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { tryMirrors, raceMirrors, currentFor };
|
||||
Reference in New Issue
Block a user