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:
lofyer
2026-08-03 12:13:02 +08:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent b8c8d24107
commit 3ccd044527
307 changed files with 98477 additions and 1148 deletions
+50 -13
View File
@@ -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 };