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>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
b8c8d24107
commit
3ccd044527
+50
-13
@@ -37,10 +37,21 @@ function markGood(prefix, 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 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 || ''));
|
||||
}
|
||||
|
||||
// 候选顺序:上次成功的优先,其余按原顺序,已拉黑的排到最后兜底
|
||||
@@ -72,6 +83,11 @@ async function tryMirrors(prefix, mirrors, fn) {
|
||||
return r;
|
||||
} catch (e) {
|
||||
lastErr = e;
|
||||
// 镜像可达但内容不满足时,说明换镜像也是同样结果,直接返回
|
||||
if (!isMirrorFault(e)) {
|
||||
markGood(prefix, m);
|
||||
throw e;
|
||||
}
|
||||
markBad(prefix, m);
|
||||
}
|
||||
}
|
||||
@@ -82,33 +98,54 @@ async function tryMirrors(prefix, mirrors, fn) {
|
||||
* 竞速尝试:同时向所有候选镜像发起请求,最先成功的胜出。
|
||||
* 适用于镜像多且大量失效的场景(如 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 = candidates(prefix, mirrors);
|
||||
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))
|
||||
.then(() => fn(m, ac.signal))
|
||||
.then((r) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
markGood(prefix, m);
|
||||
resolve(r);
|
||||
settle(resolve, r);
|
||||
})
|
||||
.catch((e) => {
|
||||
lastErr = e;
|
||||
markBad(prefix, m);
|
||||
if (--pending === 0 && !settled) {
|
||||
reject(lastErr || new Error('所有镜像均不可用'));
|
||||
// 输掉竞速被我们主动中止不算故障;但真实故障即使输了也要记进黑名单,
|
||||
// 否则下次仍会去竞速一个已知坏掉的镜像。
|
||||
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, currentFor };
|
||||
module.exports = { tryMirrors, raceMirrors, contentError };
|
||||
|
||||
Reference in New Issue
Block a user