新增 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>
152 lines
4.5 KiB
JavaScript
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 };
|