feat: 支持自定义 Z-Library 镜像并发布 v2.1.5

在 Z-Library 账户设置内增加镜像站点编辑入口,一行一个 HTTPS 地址,
自定义地址经 origin 规范化后优先于内置列表。镜像配置与凭据分开处理,
退出登录后仍保留;移除当前会话依赖的镜像时清理旧会话和 Cookie。

版本更新到 2.1.5,并补充存储、校验、IPC、界面与真实 Electron 保存测试。
This commit is contained in:
lofyer
2026-08-09 15:25:32 +08:00
parent 7f0eea7464
commit 488de94289
13 changed files with 265 additions and 7 deletions
+25 -1
View File
@@ -123,11 +123,32 @@ function write(creds) {
}
// 清除全部(含凭据)——用于"退出登录"
// 自定义镜像是站点配置不是凭据,退出登录后要保留,否则用户每次重新登录
// 都得重新填一遍地址,而默认域名可能全都已经失效。
function clear() {
sessionCreds = null;
const mirrors = getCustomMirrors();
for (const fp of [getFilePath(), getCredPath(), `${getFilePath()}.tmp`, `${getCredPath()}.tmp`]) {
try { fs.unlinkSync(fp); } catch (e) { /* ignore */ }
}
if (mirrors.length) {
try { atomicWrite(getFilePath(), JSON.stringify({ customMirrors: mirrors }, null, 2)); }
catch (e) { /* 保留失败不影响退出登录 */ }
}
}
function getCustomMirrors() {
const meta = readMeta();
return (meta && Array.isArray(meta.customMirrors)) ? meta.customMirrors.slice() : [];
}
// 只动镜像字段:与 setSession 同理,不重写密文,避免一次读取失败清空凭据
function setCustomMirrors(list) {
const meta = readMeta() || {};
const next = { ...meta };
if (Array.isArray(list) && list.length) next.customMirrors = list.slice();
else delete next.customMirrors;
atomicWrite(getFilePath(), JSON.stringify(next, null, 2));
}
// 只清除会话令牌,保留邮箱密码以便自动重新登录
@@ -158,4 +179,7 @@ function setSession(userId, userKey, mirror) {
atomicWrite(getFilePath(), JSON.stringify(next, null, 2));
}
module.exports = { init, read, write, clear, clearSession, hasCreds, getSession, setSession };
module.exports = {
init, read, write, clear, clearSession, hasCreds, getSession, setSession,
getCustomMirrors, setCustomMirrors
};
+49 -2
View File
@@ -43,13 +43,40 @@ function requestHeaders(mirror, includeForm = false) {
}
function getMirrors() {
const custom = (auth.read() || {}).customMirrors;
if (Array.isArray(custom) && custom.length) {
const custom = auth.getCustomMirrors();
if (custom.length) {
return custom.concat(DEFAULT_MIRRORS.filter((m) => !custom.includes(m)));
}
return DEFAULT_MIRRORS.slice();
}
const MAX_CUSTOM_MIRRORS = 20;
// 镜像地址来自用户输入,会被直接拼进请求 URL,因此必须按 origin 归一:
// 带路径、查询串或凭据的地址会让后续 apiUrl() 拼出错误甚至泄露凭据的 URL。
function normalizeMirror(value) {
const raw = String(value || '').trim();
if (!raw) return '';
let url;
try { url = new URL(raw); } catch (e) { throw new Error(`镜像地址无效:${raw}`); }
if (url.protocol !== 'https:') throw new Error(`镜像必须使用 HTTPS${raw}`);
if (url.username || url.password) throw new Error(`镜像地址不能包含账号信息:${raw}`);
return url.origin;
}
function normalizeMirrors(list) {
if (!Array.isArray(list)) throw new Error('镜像列表格式无效');
if (list.length > MAX_CUSTOM_MIRRORS) {
throw new Error(`最多只能保存 ${MAX_CUSTOM_MIRRORS} 个镜像`);
}
const out = [];
for (const item of list) {
const origin = normalizeMirror(item);
if (origin && !out.includes(origin)) out.push(origin);
}
return out;
}
function apiUrl(base, path, params = {}) {
const u = new URL(base + path);
for (const [k, v] of Object.entries(params)) {
@@ -379,5 +406,25 @@ module.exports = {
return auth.hasCreds();
},
// 默认域名变动频繁且会整批失效,用户需要能自己补上可用地址而不必等新版本
getMirrorConfig() {
return { custom: auth.getCustomMirrors(), defaults: DEFAULT_MIRRORS.slice() };
},
setMirrors(list) {
const next = normalizeMirrors(list);
const previous = auth.getCustomMirrors();
auth.setCustomMirrors(next);
// 换镜像后旧会话绑定的域名可能已不在列表里,留着会一直优先命中失效地址
const session = auth.getSession();
if (session && session.mirror && !getMirrors().includes(session.mirror)) {
auth.clearSession();
}
for (const mirror of previous) {
if (!next.includes(mirror)) clearCookies(mirror);
}
return { custom: next, defaults: DEFAULT_MIRRORS.slice() };
},
setLoginTransport
};