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
+34
-14
@@ -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 白等几秒。
|
||||
|
||||
Reference in New Issue
Block a user