Files
peoplelib/src/sources/mirror.js
T
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 3ccd044527 feat: 内置阅读器、批注笔记与 AI 助手,发布 1.3.0
新增 PDF/EPUB/MOBI/AZW 内置阅读器,PDF 分段读取支持超大文件,
批注、读书与画布笔记、封面生成与本地导入。AI 助手支持三种协议、
图像上下文与安全 Markdown 渲染,上下文范围改为 选中/当前页/全文,
页面与全文无需选中文本即可发送,全文会提示可能超出模型限制。

便携版输出目录固定为 PeopleLib-windows-x64,不再随版本号变化,
避免升级后 data/ 被遗留在旧目录。

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-03 12:13:02 +08:00

152 lines
4.5 KiB
JavaScript

// 通用镜像管理与故障转移
// 每个 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 contentError(message) {
const err = new Error(message);
err.mirrorHealthy = true;
return err;
}
function isMirrorFault(e) {
return !(e && e.mirrorHealthy);
}
// 竞速败者是被我们自己中止的,不能据此判定镜像坏掉
function isCancelled(e) {
return !!e && (e.name === 'AbortError' || /请求已取消/.test(e.message || ''));
}
// 候选顺序:上次成功的优先,其余按原顺序,已拉黑的排到最后兜底
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;
// 镜像可达但内容不满足时,说明换镜像也是同样结果,直接返回
if (!isMirrorFault(e)) {
markGood(prefix, m);
throw e;
}
markBad(prefix, m);
}
}
throw lastErr || new Error('所有镜像均不可用');
}
/**
* 竞速尝试:同时向所有候选镜像发起请求,最先成功的胜出。
* 适用于镜像多且大量失效的场景(如 LibGen),避免串行等待累加。
*/
// 竞速时只用未拉黑的镜像;全被拉黑才退回完整列表重试一轮。
function raceCandidates(prefix, mirrors) {
const c = stateOf(prefix);
const fresh = candidates(prefix, mirrors).filter((m) => m === c.current || !isBad(c, m));
return fresh.length ? fresh : mirrors.slice();
}
async function raceMirrors(prefix, mirrors, fn) {
const list = raceCandidates(prefix, mirrors);
if (!list.length) throw new Error('没有可用镜像');
// 胜出后主动中止其余在途请求,避免败者继续占用连接与代理带宽
const ac = new AbortController();
return new Promise((resolve, reject) => {
let pending = list.length;
let settled = false;
let lastErr;
const settle = (fn2, value) => {
if (settled) return;
settled = true;
ac.abort();
fn2(value);
};
for (const m of list) {
Promise.resolve()
.then(() => fn(m, ac.signal))
.then((r) => {
if (settled) return;
markGood(prefix, m);
settle(resolve, r);
})
.catch((e) => {
// 输掉竞速被我们主动中止不算故障;但真实故障即使输了也要记进黑名单,
// 否则下次仍会去竞速一个已知坏掉的镜像。
if (!isCancelled(e) && isMirrorFault(e)) markBad(prefix, m);
if (settled) return;
if (!isMirrorFault(e)) {
markGood(prefix, m);
settle(reject, e);
return;
}
lastErr = e;
if (--pending === 0) settle(reject, lastErr || new Error('所有镜像均不可用'));
});
}
});
}
module.exports = { tryMirrors, raceMirrors, contentError };