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:
lofyer
2026-07-25 14:51:10 +08:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit 1a1288ce18
33 changed files with 4640 additions and 0 deletions
+114
View File
@@ -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 };