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
+34 -14
View File
@@ -13,7 +13,7 @@ function setProxy(url) {
if (nextUrl) {
const parsed = new URL(nextUrl);
if (!/^https?:$/.test(parsed.protocol)) throw new Error('代理地址仅支持 http:// 或 https://');
nextDispatcher = new ProxyAgent({ uri: nextUrl });
nextDispatcher = new ProxyAgent({ uri: nextUrl, connectTimeout: 30000 });
}
if (dispatcher) {
dispatcher.close().catch(() => {});
@@ -28,6 +28,16 @@ function fetchWithProxy(url, options = {}) {
return undiciFetch(url, dispatcher ? { ...options, dispatcher } : options);
}
function fetchWithElectron(url, options = {}) {
try {
const electron = require('electron');
if (electron && electron.net && typeof electron.net.fetch === 'function') {
return electron.net.fetch(url, options);
}
} catch (e) { /* Node 测试环境没有 Electron 网络栈 */ }
return fetchWithProxy(url, options);
}
// 简易 cookie jar: Map<domain, Map<name, value>>
const cookieJar = new Map();
@@ -55,10 +65,13 @@ function setCookies(url, setCookieHeaders) {
}
}
function clearCookies(urlPrefix) {
if (!urlPrefix) { cookieJar.clear(); return; }
for (const k of cookieJar.keys()) {
if (k.includes(urlPrefix)) cookieJar.delete(k);
// target 可以是完整 URL 或裸主机名;jar 以主机名为键,
// 传 URL 时要先取出 hostname,否则永远匹配不到。
function clearCookies(target) {
if (!target) { cookieJar.clear(); return; }
const host = domainOf(target) || String(target);
for (const k of [...cookieJar.keys()]) {
if (k === host || k.endsWith(`.${host}`)) cookieJar.delete(k);
}
}
@@ -74,24 +87,27 @@ async function fetchRaw(url, options = {}) {
};
if (cookie && !headers.Cookie) headers.Cookie = cookie;
const { timeout, ...rest } = options;
const { timeout, signal: outerSignal, useElectronNet, ...rest } = options;
const ms = timeout === undefined ? DEFAULT_TIMEOUT : timeout;
let signal = rest.signal;
let timer = null;
if (!signal && ms > 0) {
const ac = new AbortController();
signal = ac.signal;
timer = setTimeout(() => ac.abort(), ms);
// 调用方传入的 signal 不能顶替超时,否则外部取消一旦启用就再也没有超时保护
const ac = new AbortController();
const abort = () => ac.abort();
if (outerSignal) {
if (outerSignal.aborted) ac.abort();
else outerSignal.addEventListener('abort', abort, { once: true });
}
const timer = ms > 0 ? setTimeout(abort, ms) : null;
try {
const res = await fetchWithProxy(url, { redirect: 'follow', ...rest, headers, signal });
const request = useElectronNet ? fetchWithElectron : fetchWithProxy;
const res = await request(url, { redirect: 'follow', ...rest, headers, signal: ac.signal });
const setCookie = res.headers.getSetCookie ? res.headers.getSetCookie() : [];
setCookies(url, setCookie);
return res;
} catch (e) {
if (e && (e.name === 'AbortError' || /abort/i.test(e.message || ''))) {
if (outerSignal && outerSignal.aborted) throw new Error('请求已取消');
throw new Error('请求超时,站点无响应');
}
// undici / Chromium 的底层网络错误信息很不友好,统一换成可读文案
@@ -102,6 +118,7 @@ async function fetchRaw(url, options = {}) {
throw e;
} finally {
if (timer) clearTimeout(timer);
if (outerSignal) outerSignal.removeEventListener('abort', abort);
}
}
@@ -181,8 +198,11 @@ function tooShort(keyword, min = 3) {
}
// 可自愈的错误:超时、网络抖动、5xx、限流。4xx 属请求本身的问题,重试无意义。
// 主动取消(竞速败者、切换页面)不是故障,重试只会浪费一次请求。
function isRetryable(e) {
return /超时|网络连接失败|站点内部错误|站点网关|暂时不可用|过于频繁/.test((e && e.message) || '');
const msg = (e && e.message) || '';
if (/请求已取消/.test(msg)) return false;
return /超时|网络连接失败|站点内部错误|站点网关|暂时不可用|过于频繁/.test(msg);
}
// 带退避的重试包装。仅在错误可自愈时重试,避免为 4xx 白等几秒。