合并zip-12:保留专用最小化签到窗口,恢复被回退的403修复/更新检查/无标签页直连/主题UI(扩展v2.17)
This commit is contained in:
1 parent
66c60c1a7a
commit
d1f4ceb528
9 files changed
+285
-33
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "daily-checkin-panel",
|
||||
"version": "2.11.0",
|
||||
"version": "2.13.0",
|
||||
"private": true,
|
||||
"description": "跑在 Cloudflare Workers 上的自动签到面板:网页后台管账号 + D1 存储 + 定时签到 + 推送通知",
|
||||
"type": "module",
|
||||
|
||||
@@ -99,6 +99,7 @@ async function getConfig() {
|
||||
// 从面板获取待执行任务
|
||||
async function fetchJobs(panelUrl, apiKey) {
|
||||
const resp = await fetch(panelUrl + '/api/external/browser-jobs', {
|
||||
credentials: 'omit', // API Key 鉴权,不带面板会话 Cookie(避免 CSRF 检查误伤)
|
||||
headers: { 'X-Api-Key': apiKey },
|
||||
});
|
||||
if (!resp.ok) throw new Error('获取任务失败:HTTP ' + resp.status);
|
||||
@@ -534,6 +535,7 @@ async function reportCookieRotation(panelUrl, apiKey, job, host) {
|
||||
const d = diffRotated(fresh, job.cookie);
|
||||
if (!d) return; // 没变化,一个请求都不发
|
||||
const resp = await fetch(panelUrl + '/api/external/creds-rotation', {
|
||||
credentials: 'omit', // API Key 鉴权,不带面板会话 Cookie
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Api-Key': apiKey },
|
||||
body: JSON.stringify({ account_id: job.account_id, domain: 'https://' + host + '/', cookies: d.changed }),
|
||||
@@ -1082,6 +1084,7 @@ async function fetchRelayJobs(panelUrl, apiKey, waitMs = RELAY_LONGPOLL_MS) {
|
||||
let ver = '';
|
||||
try { ver = (chrome.runtime.getManifest() || {}).version || ''; } catch { /* 忽略 */ }
|
||||
const resp = await fetch(panelUrl + '/api/external/relay-pending?wait=' + waitMs + (ver ? '&v=' + encodeURIComponent(ver) : ''), {
|
||||
credentials: 'omit', // API Key 鉴权,不带面板会话 Cookie
|
||||
headers: { 'X-Api-Key': apiKey },
|
||||
});
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
@@ -1110,6 +1113,7 @@ async function submitRelayResult(panelUrl, apiKey, jobId, result) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
const resp = await fetch(panelUrl + '/api/external/relay/' + jobId + '/result', {
|
||||
credentials: 'omit', // API Key 鉴权,不带面板会话 Cookie(2026-09-29:带 sid 会被面板 CSRF 检查 403)
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Api-Key': apiKey },
|
||||
body,
|
||||
@@ -1198,6 +1202,9 @@ function sanitizeHeaders(raw) {
|
||||
// 只有主站的 robots.txt / portal.php / home.php 全部超时)。
|
||||
// 原因是挑战/验证脚本在后台标签里被节流,服务端一直等不到「验证完成」,连接就不回包。
|
||||
// 所以:第一次用短超时快速试探;若超时,就把标签**激活到前台**再试一次,跑完把焦点还给用户。
|
||||
// 【2026-09-29】绝大多数站点只是普通的 Cookie API 请求,不需要标签页。
|
||||
// 改成:优先在 service worker 里直接 fetch(<all_urls> 权限 + credentials:include 会自动带 Cookie,
|
||||
// 不开任何标签页);只有直接 fetch 超时/失败、或任务显式要求前台时,才回退到标签页路径。
|
||||
async function executeRelayJob(job) {
|
||||
// 三道闸门(见上面说明):不合法的任务直接回传错误,不照做
|
||||
const bad = checkRelayJob(job);
|
||||
@@ -1206,6 +1213,80 @@ async function executeRelayJob(job) {
|
||||
const domain = url.hostname;
|
||||
const method = String(job.method || 'GET').toUpperCase();
|
||||
const reqHeaders = sanitizeHeaders(job.headers);
|
||||
const needsForeground = !!(job.options && job.options.foreground);
|
||||
|
||||
// 纯 fetch 实现:service worker 里直接跑,不依赖任何标签页/DOM。
|
||||
// (原来是包在 executeScript 里丢进标签页执行的,逻辑一字未动,只是换了个运行位置。)
|
||||
const directFetch = async (url, method, headers, bodyB64, options, timeoutMs, maxBytes) => {
|
||||
const b64ToBytes = (b64) => {
|
||||
const s = atob(b64);
|
||||
const arr = new Uint8Array(s.length);
|
||||
for (let i = 0; i < s.length; i++) arr[i] = s.charCodeAt(i);
|
||||
return arr;
|
||||
};
|
||||
const bytesToB64 = (bytes) => {
|
||||
let s = '';
|
||||
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
||||
return btoa(s);
|
||||
};
|
||||
const init = {
|
||||
method,
|
||||
headers,
|
||||
credentials: 'include', // 始终携带用户 Cookie
|
||||
redirect: options.redirect === 'manual' ? 'follow' : (options.redirect || 'follow'),
|
||||
};
|
||||
if (bodyB64) init.body = b64ToBytes(bodyB64);
|
||||
const ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
||||
if (ctrl) init.signal = ctrl.signal;
|
||||
let timer = null;
|
||||
const timeout = new Promise((resolve) => {
|
||||
timer = setTimeout(() => {
|
||||
try { if (ctrl) ctrl.abort(); } catch { /* 忽略 */ }
|
||||
resolve({ timeout: true });
|
||||
}, timeoutMs || 18000);
|
||||
});
|
||||
const call = (async () => {
|
||||
const resp = await fetch(url, init);
|
||||
const buf = new Uint8Array(await resp.arrayBuffer());
|
||||
if (maxBytes && buf.length > maxBytes) return { tooLarge: buf.length, status: resp.status };
|
||||
const h = {};
|
||||
resp.headers.forEach((v, k) => { h[k] = v; });
|
||||
return {
|
||||
status: resp.status,
|
||||
headers: h,
|
||||
body_base64: bytesToB64(buf),
|
||||
url: resp.url,
|
||||
};
|
||||
})();
|
||||
try {
|
||||
return await Promise.race([call, timeout]);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
call.catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
// 第一步:不开标签页,直接在 service worker 里试一次(短超时快速试探)。
|
||||
// 任务显式要求前台(反爬挑战站)时跳过这步,直接走标签页。
|
||||
if (!needsForeground) {
|
||||
try {
|
||||
const r = await directFetch(url.href, method, reqHeaders, job.body_base64 || null, job.options || {}, RELAY_FIRST_TRY_MS, RELAY_MAX_BYTES);
|
||||
if (r && !r.timeout) {
|
||||
if (r.tooLarge) {
|
||||
return { error: `响应体过大(${Math.round(r.tooLarge / 1048576)}MB,上限 ${Math.round(RELAY_MAX_BYTES / 1048576)}MB),已放弃回传` };
|
||||
}
|
||||
const respHeaders = { ...(r.headers || {}), 'x-relay-url': r.url || job.url };
|
||||
console.log('[签到面板] 中继请求直接完成(无标签页):', method, url.hostname);
|
||||
return { status: r.status, headers: respHeaders, body_base64: r.body_base64 };
|
||||
}
|
||||
console.log('[签到面板] 直接请求超时,回退到标签页重试:', url.hostname);
|
||||
} catch (e) {
|
||||
console.log('[签到面板] 直接请求失败,回退到标签页重试:', (e && e.message) || e);
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步(回退):走标签页执行(反爬挑战站需要真实页面环境)。
|
||||
// 标签页开在专用的最小化窗口里(见 openOwnTab),用户看不到。
|
||||
let tab = null;
|
||||
let created = false;
|
||||
let prevActiveId = null;
|
||||
|
||||
@@ -33,5 +33,5 @@
|
||||
"alarms",
|
||||
"tabs"
|
||||
],
|
||||
"version": "2.11"
|
||||
"version": "2.17"
|
||||
}
|
||||
+82
-18
@@ -3,33 +3,97 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { width: 320px; padding: 16px; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; margin: 0; background: #0f1420; color: #e8edf7; }
|
||||
h3 { margin: 0 0 12px; font-size: 15px; }
|
||||
.muted { color: #8b98b3; font-size: 12px; line-height: 1.6; }
|
||||
button { width: 100%; padding: 11px; margin: 6px 0; border: 0; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
|
||||
.primary { background: linear-gradient(135deg, #5b8cff, #4a7de8); color: #fff; }
|
||||
.ghost { background: transparent; border: 1px solid #26314a; color: #e8edf7; }
|
||||
button:hover { opacity: .9; }
|
||||
* { box-sizing: border-box; }
|
||||
/* 浅色主题(默认) */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-input: #f4f6fb;
|
||||
--text: #1a2333;
|
||||
--text-muted: #6b7690;
|
||||
--border: #dfe5f0;
|
||||
--border-strong: #c4cede;
|
||||
--accent: #3b72e8;
|
||||
--accent-hover: #2f62cf;
|
||||
--accent-soft: rgba(59,114,232,.1);
|
||||
--accent-text: #3b72e8;
|
||||
--divider: #eceff5;
|
||||
--warn-bg: rgba(176,116,0,.08);
|
||||
--warn-border: rgba(176,116,0,.3);
|
||||
--warn-text: #8a5f00;
|
||||
--ok: #1a9e54;
|
||||
--err: #d64545;
|
||||
}
|
||||
/* 深色主题:跟随系统 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0f1420;
|
||||
--bg-input: #0d1322;
|
||||
--text: #e8edf7;
|
||||
--text-muted: #8b98b3;
|
||||
--border: #26314a;
|
||||
--border-strong: #35436a;
|
||||
--accent: #5b8cff;
|
||||
--accent-hover: #4a7de8;
|
||||
--accent-soft: rgba(91,140,255,.12);
|
||||
--accent-text: #7aa5ff;
|
||||
--divider: #1c2538;
|
||||
--warn-bg: rgba(255,176,32,.08);
|
||||
--warn-border: rgba(255,176,32,.25);
|
||||
--warn-text: #e8c07a;
|
||||
--ok: #3ddc84;
|
||||
--err: #ff6b6b;
|
||||
}
|
||||
}
|
||||
body { width: 320px; padding: 16px; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; margin: 0; background: var(--bg); color: var(--text); }
|
||||
.header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
||||
.header h3 { margin: 0; font-size: 15px; font-weight: 700; }
|
||||
.header .ver { font-size: 12px; color: var(--text-muted); font-weight: 400; }
|
||||
.update-btn { margin-left: auto; width: auto; padding: 4px 10px; margin-bottom: 0; font-size: 12px; font-weight: 500; background: var(--accent-soft); border: 1px solid var(--accent); color: var(--accent-text); border-radius: 12px; cursor: pointer; }
|
||||
.update-btn:hover { filter: brightness(1.08); }
|
||||
.update-tip { font-size: 12px; line-height: 1.6; margin: 0 0 10px; padding: 8px 10px; border-radius: 8px; background: var(--warn-bg); border: 1px solid var(--warn-border); color: var(--warn-text); }
|
||||
.update-tip .dl-btn { display: inline-block; margin-top: 6px; padding: 6px 14px; background: var(--accent); color: #fff; border-radius: 6px; font-weight: 600; cursor: pointer; border: 0; font-size: 13px; }
|
||||
.update-tip .dl-btn:hover { background: var(--accent-hover); }
|
||||
.domain { font-size: 12px; color: var(--text-muted); line-height: 1.6; margin-bottom: 12px; word-break: break-all; }
|
||||
.domain .count { display: inline-block; background: var(--accent-soft); color: var(--accent-text); padding: 1px 8px; border-radius: 10px; font-size: 11px; margin-left: 4px; }
|
||||
/* 按钮统一:都是描边风格,主按钮用强调色描边+淡底色,比次按钮更显眼 */
|
||||
button { width: 100%; padding: 11px; margin: 0 0 8px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; border: 1px solid transparent; background: transparent; }
|
||||
.primary { border-color: var(--accent); color: var(--accent-text); background: var(--accent-soft); }
|
||||
.primary:hover { filter: brightness(1.08); border-color: var(--accent-hover); }
|
||||
.secondary { border-color: var(--border-strong); color: var(--text); }
|
||||
.secondary:hover { border-color: var(--accent); color: var(--accent-text); }
|
||||
button:disabled { opacity: .5; cursor: wait; }
|
||||
#status { font-size: 12px; color: #8b98b3; margin-top: 8px; min-height: 18px; }
|
||||
#status.ok { color: #3ddc84; }
|
||||
#status.err { color: #ff6b6b; }
|
||||
label { font-size: 12px; color: #8b98b3; display: block; margin: 10px 0 4px; }
|
||||
input { width: 100%; box-sizing: border-box; background: #0d1322; border: 1px solid #26314a; color: #e8edf7; border-radius: 8px; padding: 9px 12px; font-size: 13px; }
|
||||
.count { display: inline-block; background: rgba(91,140,255,.15); color: #7aa5ff; padding: 2px 10px; border-radius: 12px; font-size: 12px; margin-left: 6px; }
|
||||
.note { font-size: 12px; color: var(--text-muted); line-height: 1.6; margin: 0 0 12px; }
|
||||
.divider { border: 0; border-top: 1px solid var(--divider); margin: 4px 0 12px; }
|
||||
.section-title { font-size: 13px; font-weight: 700; margin: 0 0 8px; }
|
||||
label { font-size: 12px; color: var(--text-muted); display: block; margin: 0 0 4px; }
|
||||
input { width: 100%; background: var(--bg-input); border: 1px solid var(--border); color: var(--text); border-radius: 8px; padding: 9px 12px; font-size: 13px; margin-bottom: 10px; }
|
||||
input:focus { outline: none; border-color: var(--accent); }
|
||||
input::placeholder { color: var(--text-muted); opacity: .7; }
|
||||
#status { font-size: 12px; color: var(--text-muted); margin-top: 4px; min-height: 18px; line-height: 1.5; }
|
||||
#status.ok { color: var(--ok); }
|
||||
#status.err { color: var(--err); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3>🖊️ 签到面板助手 <span class="muted" id="title-ver"></span> <span class="count" id="count"></span></h3>
|
||||
<div class="muted" id="domain">正在读取当前网站…</div>
|
||||
<div class="header">
|
||||
<h3>🍪 签到面板助手</h3>
|
||||
<span class="ver" id="title-ver"></span>
|
||||
<button class="update-btn" id="btn-update" style="display:none" title="检查扩展更新">🔄 检查更新</button>
|
||||
</div>
|
||||
<div id="update-tip" class="update-tip" style="display:none"></div>
|
||||
<div class="domain"><span id="domain">正在读取当前网站…</span><span class="count" id="count" style="display:none"></span></div>
|
||||
|
||||
<button class="primary" id="btn-send">一键发送到签到面板</button>
|
||||
<button class="ghost" id="btn-copy">📋 一键复制全部信息(含 UA)</button>
|
||||
<div class="muted" style="margin-top:-2px">发送方式是「一次性交接码」:凭据不进地址栏,短码 5 分钟内有效、取一次即作废。</div>
|
||||
<button class="ghost" id="btn-check-conn">🔌 面板连接检查</button>
|
||||
<button class="secondary" id="btn-copy">📋 一键复制全部信息(含 UA)</button>
|
||||
<p class="note">发送方式是「一次性交接码」:凭据不进地址栏,短码 5 分钟内有效、取一次即作废。</p>
|
||||
|
||||
<hr class="divider">
|
||||
<div class="section-title">面板连接</div>
|
||||
<label>签到面板地址</label>
|
||||
<input id="panel-url" placeholder="https://daily-checkin-panel.xxx.workers.dev">
|
||||
<label>API Key(面板设置页获取)</label>
|
||||
<input id="api-key" type="password" placeholder="填入后面板点「执行」即用你的本机网络发请求">
|
||||
<button class="secondary" id="btn-check-conn">🔌 检查连接</button>
|
||||
<div id="status"></div>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
|
||||
+78
-2
@@ -175,7 +175,13 @@ async function init() {
|
||||
window._pageUA = pageUA;
|
||||
window._localStore = localStore;
|
||||
const lsCount = Object.keys(localStore).length;
|
||||
$('count').textContent = cookies.length + ' 个';
|
||||
const countEl = $('count');
|
||||
if (cookies.length) {
|
||||
countEl.textContent = cookies.length + ' 个 Cookie';
|
||||
countEl.style.display = 'inline-block';
|
||||
} else {
|
||||
countEl.style.display = 'none';
|
||||
}
|
||||
status(
|
||||
cookies.length
|
||||
? `已读取 ${cookies.length} 个 Cookie(${stat.domains} 个域 / ${stat.httpOnly} 个 HttpOnly)` + (lsCount ? ` + ${lsCount} 项 localStorage` : '')
|
||||
@@ -235,6 +241,7 @@ $('btn-send').onclick = async () => {
|
||||
if (apiKey) {
|
||||
try {
|
||||
const resp = await fetch(panelUrl + '/api/external/handoff', {
|
||||
credentials: 'omit', // API Key 鉴权,不带面板会话 Cookie
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Api-Key': apiKey },
|
||||
body: JSON.stringify(payload),
|
||||
@@ -280,6 +287,74 @@ $('api-key').onchange = saveCfgNow;
|
||||
|
||||
fillVersion();
|
||||
|
||||
// ---------- 扩展更新检查 ----------
|
||||
// Chrome 不允许未上架的扩展自己静默更新,所以这里做「检查 + 一键下载」:
|
||||
// 有新版时显示下载按钮,用户解压覆盖后去 chrome://extensions 点「重新加载」即可。
|
||||
// 版本号比对:按点分割逐段比数字("2.9" < "2.10" < "2.11")。
|
||||
function cmpVer(a, b) {
|
||||
const pa = String(a || '').split('.').map((x) => parseInt(x, 10) || 0);
|
||||
const pb = String(b || '').split('.').map((x) => parseInt(x, 10) || 0);
|
||||
const n = Math.max(pa.length, pb.length);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const d = (pa[i] || 0) - (pb[i] || 0);
|
||||
if (d) return d;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function checkExtUpdate() {
|
||||
let localVer = '';
|
||||
try { localVer = String((chrome.runtime.getManifest() || {}).version || ''); } catch { /* 忽略 */ }
|
||||
const btn = $('btn-update');
|
||||
const tip = $('update-tip');
|
||||
if (!localVer) return;
|
||||
// 面板地址:优先用输入框里已填的,没有就用注入的默认值
|
||||
let panelUrl = '';
|
||||
try {
|
||||
panelUrl = normalizePanelUrl($('panel-url').value);
|
||||
if (!panelUrl && typeof DEFAULT_PANEL_URL !== 'undefined' && DEFAULT_PANEL_URL.startsWith('http')) {
|
||||
panelUrl = normalizePanelUrl(DEFAULT_PANEL_URL);
|
||||
}
|
||||
} catch { /* 忽略 */ }
|
||||
if (!panelUrl) return; // 没填面板地址就不检查
|
||||
let remoteVer = '';
|
||||
try {
|
||||
const resp = await fetch(panelUrl + '/api/external/ext-version');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json().catch(() => null);
|
||||
remoteVer = String((data && data.version) || '');
|
||||
}
|
||||
} catch { /* 面板不可达就静默跳过 */ }
|
||||
if (!remoteVer || cmpVer(remoteVer, localVer) <= 0) return; // 没有新版
|
||||
// 有新版:显示更新按钮和提示
|
||||
btn.style.display = 'inline-block';
|
||||
tip.style.display = 'block';
|
||||
tip.innerHTML = '';
|
||||
const t1 = document.createElement('div');
|
||||
t1.textContent = `发现新版本 v${remoteVer}(当前 v${localVer})`;
|
||||
tip.appendChild(t1);
|
||||
const dl = document.createElement('button');
|
||||
dl.className = 'dl-btn';
|
||||
dl.textContent = '⬇️ 下载新版扩展';
|
||||
dl.onclick = () => {
|
||||
chrome.tabs.create({ url: panelUrl + '/cookie-helper-extension.zip' });
|
||||
const t2 = document.createElement('div');
|
||||
t2.style.marginTop = '6px';
|
||||
t2.textContent = '下载后解压覆盖原文件夹,再去 chrome://extensions 点「重新加载」即可。';
|
||||
// 只追加一次
|
||||
if (!tip.querySelector('.done-tip')) {
|
||||
t2.className = 'done-tip';
|
||||
tip.appendChild(t2);
|
||||
}
|
||||
};
|
||||
tip.appendChild(dl);
|
||||
btn.onclick = () => {
|
||||
tip.style.display = tip.style.display === 'none' ? 'block' : 'none';
|
||||
};
|
||||
}
|
||||
|
||||
checkExtUpdate();
|
||||
|
||||
// 打开弹窗时顺手叫醒一次中继(后台常驻长轮询,本来就会自己跑)。
|
||||
// 以前这里还有个「立即执行待办签到」按钮——它会挂住弹窗、也让用户以为要手动点才干活,已删掉:
|
||||
// 面板点「执行」时后台会在 1 秒内接到单,不需要在弹窗上再点一次。
|
||||
@@ -305,6 +380,7 @@ $('btn-check-conn').onclick = async () => {
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetch(panelUrl + '/api/external/ping', {
|
||||
credentials: 'omit', // API Key 鉴权,不带面板会话 Cookie
|
||||
headers: { 'X-Api-Key': apiKey },
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -316,7 +392,7 @@ $('btn-check-conn').onclick = async () => {
|
||||
// ③ 解析任务列表
|
||||
let data;
|
||||
try { data = await resp.json(); } catch { return status('连接失败:面板返回数据格式错误', 'err'); }
|
||||
status(`连接正常 ✅ API Key 有效(面板版本 ${data.version || '未知'})`, 'ok');
|
||||
status('连接正常 ✅ API Key 有效', 'ok');
|
||||
} catch (e) {
|
||||
status('检查失败:' + (e.message || e), 'err');
|
||||
}
|
||||
|
||||
+15
-1
@@ -3960,6 +3960,18 @@ $('btn-tg-clear').onclick = async () => {
|
||||
// 只有在线时,账号才能切到「本地网络」——否则请求没人接单,点了也只是白等。
|
||||
let RELAY_ONLINE = false;
|
||||
let RELAY_VERSION = '';
|
||||
let RELAY_LATEST = '';
|
||||
// 版本号比对:按点分割逐段比数字("2.9" < "2.10" < "2.11")
|
||||
function cmpVer(a, b) {
|
||||
const pa = String(a || '').split('.').map((x) => parseInt(x, 10) || 0);
|
||||
const pb = String(b || '').split('.').map((x) => parseInt(x, 10) || 0);
|
||||
const n = Math.max(pa.length, pb.length);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const d = (pa[i] || 0) - (pb[i] || 0);
|
||||
if (d) return d;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
let RELAY_BACKLOG = 0; // 中继队列里还没做完的请求数(扩展单飞执行,堆积时签到会排队超时)
|
||||
async function refreshExtState() {
|
||||
const home = $('ext-state');
|
||||
@@ -3967,6 +3979,7 @@ async function refreshExtState() {
|
||||
const s = await api('/api/relay-status', 'GET');
|
||||
RELAY_ONLINE = !!s.online;
|
||||
RELAY_VERSION = s.version || '';
|
||||
RELAY_LATEST = s.latest_version || '';
|
||||
RELAY_BACKLOG = Number(s.backlog || 0);
|
||||
} catch {
|
||||
RELAY_ONLINE = false;
|
||||
@@ -3983,7 +3996,8 @@ async function refreshExtState() {
|
||||
el.textContent = RELAY_ONLINE
|
||||
? (busy
|
||||
? `🟠 扩展在线 · 中继排队中(${RELAY_BACKLOG} 个请求待执行)`
|
||||
: `🟢 扩展在线(自动中继已启用)${RELAY_VERSION ? ' · v' + RELAY_VERSION : ''}`)
|
||||
: `🟢 扩展在线(自动中继已启用)${RELAY_VERSION ? ' · v' + RELAY_VERSION : ''}`
|
||||
+ (RELAY_LATEST && RELAY_VERSION && cmpVer(RELAY_LATEST, RELAY_VERSION) > 0 ? `(有新版 v${RELAY_LATEST},去扩展弹窗点更新)` : ''))
|
||||
: '⚪ 扩展离线(走 CF 网络)';
|
||||
}
|
||||
return RELAY_ONLINE;
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
// 自动生成:扩展源文件嵌入(运行 node tools/gen-ext-files.mjs 更新)
|
||||
// 源文件在 public/ext-src/;下载时由 src/ext-zip.js 注入面板地址。
|
||||
export const EXT_FILES = {
|
||||
"manifest.json": "{\n \"action\": {\n \"default_popup\": \"popup.html\",\n \"default_title\": \"签到面板助手:一键取 Cookie、本地网络中继、浏览器签到\",\n \"default_icon\": {\n \"16\": \"icon16.png\",\n \"32\": \"icon32.png\",\n \"48\": \"icon48.png\",\n \"128\": \"icon128.png\"\n }\n },\n \"background\": {\n \"service_worker\": \"background.js\"\n },\n \"description\": \"签到面板助手:一键取 Cookie、本地网络中继、需要时自动完成浏览器签到\",\n \"minimum_chrome_version\": \"110\",\n \"host_permissions\": [\n \"<all_urls>\"\n ],\n \"icons\": {\n \"16\": \"icon16.png\",\n \"32\": \"icon32.png\",\n \"48\": \"icon48.png\",\n \"128\": \"icon128.png\"\n },\n \"manifest_version\": 3,\n \"name\": \"签到面板助手\",\n \"permissions\": [\n \"cookies\",\n \"activeTab\",\n \"scripting\",\n \"storage\",\n \"alarms\",\n \"tabs\"\n ],\n \"version\": \"2.11\"\n}\n",
|
||||
"popup.html": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n<style>\n body { width: 320px; padding: 16px; font-family: -apple-system, \"PingFang SC\", \"Microsoft YaHei\", sans-serif; margin: 0; background: #0f1420; color: #e8edf7; }\n h3 { margin: 0 0 12px; font-size: 15px; }\n .muted { color: #8b98b3; font-size: 12px; line-height: 1.6; }\n button { width: 100%; padding: 11px; margin: 6px 0; border: 0; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }\n .primary { background: linear-gradient(135deg, #5b8cff, #4a7de8); color: #fff; }\n .ghost { background: transparent; border: 1px solid #26314a; color: #e8edf7; }\n button:hover { opacity: .9; }\n button:disabled { opacity: .5; cursor: wait; }\n #status { font-size: 12px; color: #8b98b3; margin-top: 8px; min-height: 18px; }\n #status.ok { color: #3ddc84; }\n #status.err { color: #ff6b6b; }\n label { font-size: 12px; color: #8b98b3; display: block; margin: 10px 0 4px; }\n input { width: 100%; box-sizing: border-box; background: #0d1322; border: 1px solid #26314a; color: #e8edf7; border-radius: 8px; padding: 9px 12px; font-size: 13px; }\n .count { display: inline-block; background: rgba(91,140,255,.15); color: #7aa5ff; padding: 2px 10px; border-radius: 12px; font-size: 12px; margin-left: 6px; }\n</style>\n</head>\n<body>\n <h3>🖊️ 签到面板助手 <span class=\"muted\" id=\"title-ver\"></span> <span class=\"count\" id=\"count\"></span></h3>\n <div class=\"muted\" id=\"domain\">正在读取当前网站…</div>\n <button class=\"primary\" id=\"btn-send\">一键发送到签到面板</button>\n <button class=\"ghost\" id=\"btn-copy\">📋 一键复制全部信息(含 UA)</button>\n <div class=\"muted\" style=\"margin-top:-2px\">发送方式是「一次性交接码」:凭据不进地址栏,短码 5 分钟内有效、取一次即作废。</div>\n <button class=\"ghost\" id=\"btn-check-conn\">🔌 面板连接检查</button>\n <label>签到面板地址</label>\n <input id=\"panel-url\" placeholder=\"https://daily-checkin-panel.xxx.workers.dev\">\n <label>API Key(面板设置页获取)</label>\n <input id=\"api-key\" type=\"password\" placeholder=\"填入后面板点「执行」即用你的本机网络发请求\">\n <div id=\"status\"></div>\n <script src=\"popup.js\"></script>\n</body>\n</html>\n",
|
||||
"popup.js": "// 签到面板 Cookie 助手 - popup.js\n// DEFAULT_PANEL_URL 由面板在用户下载时动态注入(替换 https://your-panel.your-name.workers.dev 占位符),\n// 扩展首次打开时自动带出,用户仍可手动修改。\nconst DEFAULT_PANEL_URL = '__PANEL_URL__';\n// API Key 不从下载包里注入:由用户在面板「设置」页复制后手动填入,避免 Key 随文件传播、也方便随时更换。\nconst $ = (id) => document.getElementById(id);\nconst status = (msg, cls = '') => { const s = $('status'); s.textContent = msg; s.className = cls; };\n\nlet cookies = [];\nlet domain = '';\n\n// 标题里的版本号从 manifest 读:以前 popup 里写死「2.2」而 manifest 已经是 2.3,\n// 面板上显示「扩展在线 · v2.3」、扩展里写 2.2,看着像两个东西。\nfunction fillVersion() {\n try {\n const el = $('title-ver');\n if (el) el.textContent = 'v' + ((chrome.runtime.getManifest() || {}).version || '');\n } catch { /* 忽略 */ }\n}\n\n// ---------- 配置读写 ----------\n// API Key 属于凭据:存 storage.local(只在本机,不会跟着浏览器账号同步上云)。\n// 老版本(≤ 2.3)把它写在 sync:这里读不到本地就回退读一次 sync,并搬过来、删掉云端那份。\nfunction localArea() {\n try {\n const l = chrome.storage && chrome.storage.local;\n if (l && typeof l.get === 'function' && typeof l.set === 'function') return l;\n } catch { /* 忽略 */ }\n return chrome.storage.sync;\n}\nasync function readCfg() {\n let got = {};\n try { got = (await localArea().get(['panelUrl', 'apiKey'])) || {}; } catch { got = {}; }\n if (!got.apiKey) {\n try {\n const s = await chrome.storage.sync.get(['apiKey']);\n if (s && s.apiKey) {\n got.apiKey = s.apiKey;\n try { await localArea().set({ apiKey: s.apiKey }); } catch { /* 忽略 */ }\n try { if (typeof chrome.storage.sync.remove === 'function') await chrome.storage.sync.remove(['apiKey']); } catch { /* 忽略 */ }\n }\n } catch { /* 忽略 */ }\n }\n return got;\n}\nasync function writeCfg(obj) {\n try { await localArea().set(obj); } catch { /* 忽略 */ }\n}\n\n// 面板地址规范化 + 校验。Cookie / API Key 都要发到这里,\n// 所以除本机回环调试外一律要求 https(http 会让凭据在网络上明文传输)。\nfunction normalizePanelUrl(raw) {\n const s = String(raw || '').trim().replace(/\\/+$/, '');\n if (!s) return '';\n try {\n const u = new URL(s);\n if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';\n const path = u.pathname && u.pathname !== '/' ? u.pathname.replace(/\\/+$/, '') : '';\n return u.origin + path;\n } catch { return ''; }\n}\nfunction panelUrlProblem(url) {\n let u;\n try { u = new URL(url); } catch { return '面板地址不是合法网址(示例:https://xxx.workers.dev)'; }\n const loop = u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '::1' || u.hostname === '[::1]';\n if (u.protocol !== 'https:' && !loop) return '面板地址必须是 https —— 否则 Cookie 和 API Key 会明文上网';\n return '';\n}\n// 读取输入框里的面板地址并校验;不合格时把提示写进状态栏并返回空串\nasync function panelUrlFromInput(showErr = status) {\n let url = normalizePanelUrl($('panel-url').value);\n if (!url) { showErr('请先填写签到面板地址(示例:https://xxx.workers.dev)', 'err'); return ''; }\n const problem = panelUrlProblem(url);\n if (problem) { showErr(problem, 'err'); return ''; }\n await writeCfg({ panelUrl: url });\n return url;\n}\n\n// 采集当前标签页的 Cookie,力求“精准而全面”:\n// ① 主源 getAll({ url }) —— 与浏览器真正会发给该地址的 Cookie 完全一致\n// (自动考虑 domain 匹配含父域、path、secure、sameSite、是否过期),对任意网站都准。\n// ② host-only 兜底 getAll({ domain: hostname })。\n// ③ 尝试分区 Cookie(CHIPS),失败则跳过。\n// 合并去重(name|domain|path),并按“路径长→域名→名称”排序,尽量贴近浏览器发送顺序。\n// 刻意不猜父域(避免把 co.uk / com.cn 这种当成域名,反而混入无关 Cookie)。\nasync function collectCookies(tab) {\n const bag = new Map();\n const put = (list) => { for (const c of list || []) bag.set(`${c.name}|${c.domain}|${c.path}`, c); };\n let host = '';\n let origin = '';\n try { const u = new URL(tab.url); host = u.hostname; origin = u.origin; } catch { /* 忽略 */ }\n\n const queries = [];\n if (tab.url) queries.push({ url: tab.url });\n if (host) queries.push({ domain: host });\n if (origin) queries.push({ url: tab.url, partitionKey: { topLevelSite: origin } });\n\n for (const q of queries) {\n try { put(await chrome.cookies.getAll(q)); } catch { /* 忽略不支持的查询 */ }\n }\n\n const list = Array.from(bag.values());\n list.so
Line truncated
|
||||
"background.js": "// 签到面板 Cookie 助手 - 后台执行引擎\n// 每小时从面板获取 browser 模式的待执行任务,在用户浏览器中完成签到(使用用户网络),上报结果。\n\n// DEFAULT_PANEL_URL 由面板在用户下载时动态注入(替换 https://your-panel.your-name.workers.dev 占位符)\nconst DEFAULT_PANEL_URL = '__PANEL_URL__';\n\nconst ALARM_NAME = 'checkin-jobs';\nconst CHECK_INTERVAL_MIN = 1; // 每分钟领一次「浏览器导航签到」任务(面板点「浏览器签到」后很快就会被领走)\nconst RELAY_ALARM = 'relay-poll';\n// 注意:Chrome 会把 alarms 的周期压到最小 0.5 分钟,靠短轮询做不到「秒级响应」。\n// 所以中继改成「长轮询」:请求挂起最多 20 秒,一有任务 Worker 立刻返回;\n// 一轮结束马上接下一轮(RELAY_GAP_MS),延迟从 15~30 秒降到 1 秒内。\nconst RELAY_INTERVAL_MIN = 0.5; // 兜底 alarm(Service Worker 被回收后由它重新拉起长轮询循环)\nconst RELAY_LONGPOLL_MS = 15000; // 单次长轮询挂起时长(Worker 端上限 15 秒)\nconst RELAY_GAP_MS = 200; // 两轮长轮询之间的间隔\nconst RELAY_BURST_ROUNDS = 24; // 单次突发最多跑几轮长轮询(防止无限循环/异常时死循环)\nconst RELAY_BURST_IDLE = 6; // 连续几轮没接到任务就结束本次突发(6×15s≈90s 空转覆盖)\nconst RELAY_MAX_JOBS_PER_ROUND = 2; // 单轮最多执行几个中继任务(单飞执行,多了会集体超时)\nconst RELAY_FETCH_TIMEOUT_MS = 30000; // 页面内单次 fetch 的超时(52pojie / 糊涂鳄这类慢站需要更久)\n// 第一次尝试用较短超时:普通站点几百毫秒就回来了,只有「后台标签里反爬挑战跑不动」的站点会超时。\n// 超时后把标签激活到前台再试一次(见 executeRelayJob)。\nconst RELAY_FIRST_TRY_MS = 12000;\nconst RELAY_ACTIVATE_WAIT_MS = 1200; // 激活前台后等页面里的挑战/定时脚本醒过来\n// 任务级硬超时:超时也要回传,避免任务永久卡在 pending。\n// 【必须能装下「首次试探 + 激活前台 + 前台重试」】—— 否则第二次明明快要拿到响应了,\n// 也会被硬超时掐掉。线上就是这么坏的:45 秒的硬超时把 12 + 1.2 + 30 的重试链条掐死,\n// 面板上永远只看到「中继执行超时」,而那个 POST 其实已经发出去了(站点可能已签到)。\n// 所以下面的值用公式算,改上面任何一个常量都不会再踩这个坑。\nconst RELAY_JOB_TIMEOUT_MS = RELAY_FIRST_TRY_MS + RELAY_ACTIVATE_WAIT_MS + RELAY_FETCH_TIMEOUT_MS + 15000; // = 58.2s\n// 单轮「浏览器签到任务」的总预算:Service Worker 随时可能被浏览器回收,\n// 串着做太久会做到一半被掐掉;用完了剩下的留给下一分钟(面板每分钟都会再下发)。\nconst JOBS_BUDGET_MS = 90 * 1000;\n\n// ---------- 配置:面板地址 + API Key ----------\n//\n// 【为什么 Key 不放 storage.sync】sync 里的内容会跟着浏览器账号同步到云端,\n// 而这把 API Key 等于「让面板远程指挥你这台机器开标签页、用你的 Cookie 发请求」的钥匙,\n// 属于凭据,只应该留在本机(storage.local)。\n// 老版本(≤ 2.3)把它写在 sync:这里读不到本地就回退读一次 sync,\n// 并把它搬到 local、再把云端那份删掉(迁移只做一次)。\nfunction localArea() {\n try {\n const l = chrome.storage && chrome.storage.local;\n if (l && typeof l.get === 'function' && typeof l.set === 'function') return l;\n } catch { /* 忽略:受限环境(测试台架)没有 local,回退 sync */ }\n return chrome.storage.sync;\n}\n\nasync function readStore(keys) {\n try { return (await localArea().get(keys)) || {}; } catch { return {}; }\n}\n\nasync function writeStore(obj) {\n try { await localArea().set(obj); return true; } catch { return false; }\n}\n\n// 把 Key 从 sync 迁到 local(本地已经有就不用管)\nasync function migrateApiKeyFromSync(got) {\n const have = got || {};\n if (have.apiKey) return have;\n try {\n const s = await chrome.storage.sync.get(['apiKey']);\n if (s && s.apiKey) {\n await writeStore({ apiKey: s.apiKey });\n try { if (typeof chrome.storage.sync.remove === 'function') await chrome.storage.sync.remove(['apiKey']); } catch { /* 忽略 */ }\n return { ...have, apiKey: s.apiKey };\n }\n } catch { /* 忽略 */ }\n return have;\n}\n\n// 面板地址必须是合法的 http(s) 地址。\n// 凭据(Cookie / API Key)都要发到这儿,地址不合法就当作「没配置」——\n// 否则等于把一个未知地址当成面板,把用户 Cookie 全送过去。\nfunction normalizePanelUrl(raw) {\n const s = String(raw || '').trim().replace(/\\/+$/, '');\n if (!s) return '';\n try {\n const u = new URL(s);\n if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';\n
Line truncated
|
||||
"manifest.json": "{\n \"action\": {\n \"default_popup\": \"popup.html\",\n \"default_title\": \"签到面板助手:一键取 Cookie、本地网络中继、浏览器签到\",\n \"default_icon\": {\n \"16\": \"icon16.png\",\n \"32\": \"icon32.png\",\n \"48\": \"icon48.png\",\n \"128\": \"icon128.png\"\n }\n },\n \"background\": {\n \"service_worker\": \"background.js\"\n },\n \"description\": \"签到面板助手:一键取 Cookie、本地网络中继、需要时自动完成浏览器签到\",\n \"minimum_chrome_version\": \"110\",\n \"host_permissions\": [\n \"<all_urls>\"\n ],\n \"icons\": {\n \"16\": \"icon16.png\",\n \"32\": \"icon32.png\",\n \"48\": \"icon48.png\",\n \"128\": \"icon128.png\"\n },\n \"manifest_version\": 3,\n \"name\": \"签到面板助手\",\n \"permissions\": [\n \"cookies\",\n \"activeTab\",\n \"scripting\",\n \"storage\",\n \"alarms\",\n \"tabs\"\n ],\n \"version\": \"2.17\"\n}\n",
|
||||
"popup.html": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n<style>\n * { box-sizing: border-box; }\n /* 浅色主题(默认) */\n :root {\n --bg: #ffffff;\n --bg-input: #f4f6fb;\n --text: #1a2333;\n --text-muted: #6b7690;\n --border: #dfe5f0;\n --border-strong: #c4cede;\n --accent: #3b72e8;\n --accent-hover: #2f62cf;\n --accent-soft: rgba(59,114,232,.1);\n --accent-text: #3b72e8;\n --divider: #eceff5;\n --warn-bg: rgba(176,116,0,.08);\n --warn-border: rgba(176,116,0,.3);\n --warn-text: #8a5f00;\n --ok: #1a9e54;\n --err: #d64545;\n }\n /* 深色主题:跟随系统 */\n @media (prefers-color-scheme: dark) {\n :root {\n --bg: #0f1420;\n --bg-input: #0d1322;\n --text: #e8edf7;\n --text-muted: #8b98b3;\n --border: #26314a;\n --border-strong: #35436a;\n --accent: #5b8cff;\n --accent-hover: #4a7de8;\n --accent-soft: rgba(91,140,255,.12);\n --accent-text: #7aa5ff;\n --divider: #1c2538;\n --warn-bg: rgba(255,176,32,.08);\n --warn-border: rgba(255,176,32,.25);\n --warn-text: #e8c07a;\n --ok: #3ddc84;\n --err: #ff6b6b;\n }\n }\n body { width: 320px; padding: 16px; font-family: -apple-system, \"PingFang SC\", \"Microsoft YaHei\", sans-serif; margin: 0; background: var(--bg); color: var(--text); }\n .header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }\n .header h3 { margin: 0; font-size: 15px; font-weight: 700; }\n .header .ver { font-size: 12px; color: var(--text-muted); font-weight: 400; }\n .update-btn { margin-left: auto; width: auto; padding: 4px 10px; margin-bottom: 0; font-size: 12px; font-weight: 500; background: var(--accent-soft); border: 1px solid var(--accent); color: var(--accent-text); border-radius: 12px; cursor: pointer; }\n .update-btn:hover { filter: brightness(1.08); }\n .update-tip { font-size: 12px; line-height: 1.6; margin: 0 0 10px; padding: 8px 10px; border-radius: 8px; background: var(--warn-bg); border: 1px solid var(--warn-border); color: var(--warn-text); }\n .update-tip .dl-btn { display: inline-block; margin-top: 6px; padding: 6px 14px; background: var(--accent); color: #fff; border-radius: 6px; font-weight: 600; cursor: pointer; border: 0; font-size: 13px; }\n .update-tip .dl-btn:hover { background: var(--accent-hover); }\n .domain { font-size: 12px; color: var(--text-muted); line-height: 1.6; margin-bottom: 12px; word-break: break-all; }\n .domain .count { display: inline-block; background: var(--accent-soft); color: var(--accent-text); padding: 1px 8px; border-radius: 10px; font-size: 11px; margin-left: 4px; }\n /* 按钮统一:都是描边风格,主按钮用强调色描边+淡底色,比次按钮更显眼 */\n button { width: 100%; padding: 11px; margin: 0 0 8px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; border: 1px solid transparent; background: transparent; }\n .primary { border-color: var(--accent); color: var(--accent-text); background: var(--accent-soft); }\n .primary:hover { filter: brightness(1.08); border-color: var(--accent-hover); }\n .secondary { border-color: var(--border-strong); color: var(--text); }\n .secondary:hover { border-color: var(--accent); color: var(--accent-text); }\n button:disabled { opacity: .5; cursor: wait; }\n .note { font-size: 12px; color: var(--text-muted); line-height: 1.6; margin: 0 0 12px; }\n .divider { border: 0; border-top: 1px solid var(--divider); margin: 4px 0 12px; }\n .section-title { font-size: 13px; font-weight: 700; margin: 0 0 8px; }\n label { font-size: 12px; color: var(--text-muted); display: block; margin: 0 0 4px; }\n input { width: 100%; background: var(--bg-input); border: 1px solid var(--border); color: var(--text); border-radius: 8px; padding: 9px 12px; font-size: 13px; margin-bottom: 10px; }\n input:focus { outline: none; border-color: var(--accent); }\n input::placeholder { color: var(--text-muted); opacity: .7; }\n #status { font-size: 12px; color: var(--text-muted); margin-top: 4px; min-height: 18px; line-height: 1.5; }\n #status.ok { color: var(--ok); }\n #status.err { color: var(--err); }\n</style>\n</head>\n<body>\n <div class=\"header\">\n <h3>🍪 签到面板助手</h3>\n <span class=\"ver\" id=\"title-ver\"></span>\n <button class=\"update-btn\" id=\"btn-update\" style=\"display:none\" title=\"检查扩展更新\">🔄 检查更新</button>\n </div>\n <div id=\"update-tip\" class=\"update-tip\" style=\"display:none\"></div>\n <div class=\"domain\"><span id=\"domain\">正在读取当前网站…</span><span class=\"count\" id=\"count\" style=\"display:none\"></span></div>\n\n <button class=\"primary\" id=\"btn-send\">一键发送到签到面板</button>\n <button class=\"secondary\" id=\"btn-copy\">📋 一键复制全部信息(含 UA)</button>\n <p class=\"note\">发送方式是「一次性交接码」:凭据不进地址栏,短码 5Line truncated
|
||||
"popup.js": "// 签到面板 Cookie 助手 - popup.js\n// DEFAULT_PANEL_URL 由面板在用户下载时动态注入(替换 https://your-panel.your-name.workers.dev 占位符),\n// 扩展首次打开时自动带出,用户仍可手动修改。\nconst DEFAULT_PANEL_URL = '__PANEL_URL__';\n// API Key 不从下载包里注入:由用户在面板「设置」页复制后手动填入,避免 Key 随文件传播、也方便随时更换。\nconst $ = (id) => document.getElementById(id);\nconst status = (msg, cls = '') => { const s = $('status'); s.textContent = msg; s.className = cls; };\n\nlet cookies = [];\nlet domain = '';\n\n// 标题里的版本号从 manifest 读:以前 popup 里写死「2.2」而 manifest 已经是 2.3,\n// 面板上显示「扩展在线 · v2.3」、扩展里写 2.2,看着像两个东西。\nfunction fillVersion() {\n try {\n const el = $('title-ver');\n if (el) el.textContent = 'v' + ((chrome.runtime.getManifest() || {}).version || '');\n } catch { /* 忽略 */ }\n}\n\n// ---------- 配置读写 ----------\n// API Key 属于凭据:存 storage.local(只在本机,不会跟着浏览器账号同步上云)。\n// 老版本(≤ 2.3)把它写在 sync:这里读不到本地就回退读一次 sync,并搬过来、删掉云端那份。\nfunction localArea() {\n try {\n const l = chrome.storage && chrome.storage.local;\n if (l && typeof l.get === 'function' && typeof l.set === 'function') return l;\n } catch { /* 忽略 */ }\n return chrome.storage.sync;\n}\nasync function readCfg() {\n let got = {};\n try { got = (await localArea().get(['panelUrl', 'apiKey'])) || {}; } catch { got = {}; }\n if (!got.apiKey) {\n try {\n const s = await chrome.storage.sync.get(['apiKey']);\n if (s && s.apiKey) {\n got.apiKey = s.apiKey;\n try { await localArea().set({ apiKey: s.apiKey }); } catch { /* 忽略 */ }\n try { if (typeof chrome.storage.sync.remove === 'function') await chrome.storage.sync.remove(['apiKey']); } catch { /* 忽略 */ }\n }\n } catch { /* 忽略 */ }\n }\n return got;\n}\nasync function writeCfg(obj) {\n try { await localArea().set(obj); } catch { /* 忽略 */ }\n}\n\n// 面板地址规范化 + 校验。Cookie / API Key 都要发到这里,\n// 所以除本机回环调试外一律要求 https(http 会让凭据在网络上明文传输)。\nfunction normalizePanelUrl(raw) {\n const s = String(raw || '').trim().replace(/\\/+$/, '');\n if (!s) return '';\n try {\n const u = new URL(s);\n if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';\n const path = u.pathname && u.pathname !== '/' ? u.pathname.replace(/\\/+$/, '') : '';\n return u.origin + path;\n } catch { return ''; }\n}\nfunction panelUrlProblem(url) {\n let u;\n try { u = new URL(url); } catch { return '面板地址不是合法网址(示例:https://xxx.workers.dev)'; }\n const loop = u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '::1' || u.hostname === '[::1]';\n if (u.protocol !== 'https:' && !loop) return '面板地址必须是 https —— 否则 Cookie 和 API Key 会明文上网';\n return '';\n}\n// 读取输入框里的面板地址并校验;不合格时把提示写进状态栏并返回空串\nasync function panelUrlFromInput(showErr = status) {\n let url = normalizePanelUrl($('panel-url').value);\n if (!url) { showErr('请先填写签到面板地址(示例:https://xxx.workers.dev)', 'err'); return ''; }\n const problem = panelUrlProblem(url);\n if (problem) { showErr(problem, 'err'); return ''; }\n await writeCfg({ panelUrl: url });\n return url;\n}\n\n// 采集当前标签页的 Cookie,力求“精准而全面”:\n// ① 主源 getAll({ url }) —— 与浏览器真正会发给该地址的 Cookie 完全一致\n// (自动考虑 domain 匹配含父域、path、secure、sameSite、是否过期),对任意网站都准。\n// ② host-only 兜底 getAll({ domain: hostname })。\n// ③ 尝试分区 Cookie(CHIPS),失败则跳过。\n// 合并去重(name|domain|path),并按“路径长→域名→名称”排序,尽量贴近浏览器发送顺序。\n// 刻意不猜父域(避免把 co.uk / com.cn 这种当成域名,反而混入无关 Cookie)。\nasync function collectCookies(tab) {\n const bag = new Map();\n const put = (list) => { for (const c of list || []) bag.set(`${c.name}|${c.domain}|${c.path}`, c); };\n let host = '';\n let origin = '';\n try { const u = new URL(tab.url); host = u.hostname; origin = u.origin; } catch { /* 忽略 */ }\n\n const queries = [];\n if (tab.url) queries.push({ url: tab.url });\n if (host) queries.push({ domain: host });\n if (origin) queries.push({ url: tab.url, partitionKey: { topLevelSite: origin } });\n\n for (const q of queries) {\n try { put(await chrome.cookies.getAll(q)); } catch { /* 忽略不支持的查询 */ }\n }\n\n const list = Array.from(bag.values());\n list.so
Line truncated
|
||||
"background.js": "// 签到面板 Cookie 助手 - 后台执行引擎\n// 每小时从面板获取 browser 模式的待执行任务,在用户浏览器中完成签到(使用用户网络),上报结果。\n\n// DEFAULT_PANEL_URL 由面板在用户下载时动态注入(替换 https://your-panel.your-name.workers.dev 占位符)\nconst DEFAULT_PANEL_URL = '__PANEL_URL__';\n\nconst ALARM_NAME = 'checkin-jobs';\nconst CHECK_INTERVAL_MIN = 1; // 每分钟领一次「浏览器导航签到」任务(面板点「浏览器签到」后很快就会被领走)\nconst RELAY_ALARM = 'relay-poll';\n// 注意:Chrome 会把 alarms 的周期压到最小 0.5 分钟,靠短轮询做不到「秒级响应」。\n// 所以中继改成「长轮询」:请求挂起最多 20 秒,一有任务 Worker 立刻返回;\n// 一轮结束马上接下一轮(RELAY_GAP_MS),延迟从 15~30 秒降到 1 秒内。\nconst RELAY_INTERVAL_MIN = 0.5; // 兜底 alarm(Service Worker 被回收后由它重新拉起长轮询循环)\nconst RELAY_LONGPOLL_MS = 15000; // 单次长轮询挂起时长(Worker 端上限 15 秒)\nconst RELAY_GAP_MS = 200; // 两轮长轮询之间的间隔\nconst RELAY_BURST_ROUNDS = 24; // 单次突发最多跑几轮长轮询(防止无限循环/异常时死循环)\nconst RELAY_BURST_IDLE = 6; // 连续几轮没接到任务就结束本次突发(6×15s≈90s 空转覆盖)\nconst RELAY_MAX_JOBS_PER_ROUND = 2; // 单轮最多执行几个中继任务(单飞执行,多了会集体超时)\nconst RELAY_FETCH_TIMEOUT_MS = 30000; // 页面内单次 fetch 的超时(52pojie / 糊涂鳄这类慢站需要更久)\n// 第一次尝试用较短超时:普通站点几百毫秒就回来了,只有「后台标签里反爬挑战跑不动」的站点会超时。\n// 超时后把标签激活到前台再试一次(见 executeRelayJob)。\nconst RELAY_FIRST_TRY_MS = 12000;\nconst RELAY_ACTIVATE_WAIT_MS = 1200; // 激活前台后等页面里的挑战/定时脚本醒过来\n// 任务级硬超时:超时也要回传,避免任务永久卡在 pending。\n// 【必须能装下「首次试探 + 激活前台 + 前台重试」】—— 否则第二次明明快要拿到响应了,\n// 也会被硬超时掐掉。线上就是这么坏的:45 秒的硬超时把 12 + 1.2 + 30 的重试链条掐死,\n// 面板上永远只看到「中继执行超时」,而那个 POST 其实已经发出去了(站点可能已签到)。\n// 所以下面的值用公式算,改上面任何一个常量都不会再踩这个坑。\nconst RELAY_JOB_TIMEOUT_MS = RELAY_FIRST_TRY_MS + RELAY_ACTIVATE_WAIT_MS + RELAY_FETCH_TIMEOUT_MS + 15000; // = 58.2s\n// 单轮「浏览器签到任务」的总预算:Service Worker 随时可能被浏览器回收,\n// 串着做太久会做到一半被掐掉;用完了剩下的留给下一分钟(面板每分钟都会再下发)。\nconst JOBS_BUDGET_MS = 90 * 1000;\n\n// ---------- 配置:面板地址 + API Key ----------\n//\n// 【为什么 Key 不放 storage.sync】sync 里的内容会跟着浏览器账号同步到云端,\n// 而这把 API Key 等于「让面板远程指挥你这台机器开标签页、用你的 Cookie 发请求」的钥匙,\n// 属于凭据,只应该留在本机(storage.local)。\n// 老版本(≤ 2.3)把它写在 sync:这里读不到本地就回退读一次 sync,\n// 并把它搬到 local、再把云端那份删掉(迁移只做一次)。\nfunction localArea() {\n try {\n const l = chrome.storage && chrome.storage.local;\n if (l && typeof l.get === 'function' && typeof l.set === 'function') return l;\n } catch { /* 忽略:受限环境(测试台架)没有 local,回退 sync */ }\n return chrome.storage.sync;\n}\n\nasync function readStore(keys) {\n try { return (await localArea().get(keys)) || {}; } catch { return {}; }\n}\n\nasync function writeStore(obj) {\n try { await localArea().set(obj); return true; } catch { return false; }\n}\n\n// 把 Key 从 sync 迁到 local(本地已经有就不用管)\nasync function migrateApiKeyFromSync(got) {\n const have = got || {};\n if (have.apiKey) return have;\n try {\n const s = await chrome.storage.sync.get(['apiKey']);\n if (s && s.apiKey) {\n await writeStore({ apiKey: s.apiKey });\n try { if (typeof chrome.storage.sync.remove === 'function') await chrome.storage.sync.remove(['apiKey']); } catch { /* 忽略 */ }\n return { ...have, apiKey: s.apiKey };\n }\n } catch { /* 忽略 */ }\n return have;\n}\n\n// 面板地址必须是合法的 http(s) 地址。\n// 凭据(Cookie / API Key)都要发到这儿,地址不合法就当作「没配置」——\n// 否则等于把一个未知地址当成面板,把用户 Cookie 全送过去。\nfunction normalizePanelUrl(raw) {\n const s = String(raw || '').trim().replace(/\\/+$/, '');\n if (!s) return '';\n try {\n const u = new URL(s);\n if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';\n
Line truncated
|
||||
"@b64:icon16.png": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAZlJREFUOE+Vk0svQ0EUx8/UQqOVku4kvoQPYWFD4hmNVyteYYFF6QJBidJGU6GJhehCWViLRT+B+B5Cqvqmbe6RM3NnOtU2OIt7M/ee///8zpkZBmYsRSuIiGAgAhpN3vTfQKC8G6+DkZQ//iuWJne+TsYaiS+XW8EdLiqS6zUbPD6XIZYoKgJpwhYvyqhjk1iPyWAOYuv2mm8j/hRvg0zY/HkJZc+6eCqU50kkXohkIbrSDmOHH/UEc2dfisBmBcjkDT7IUhkh7hWVh/1pYIAAIAYo8SmPzUY+FYEkoZ4pXIEMT45vOITR/nuNmMyYJ1xUBFerbTW9NlsM7LxVZzBzWqgS6HhmZTmw3h4rPDwVoMUCUKoYgoSGOB3KCwJTHHDboMtp4QO73ezgEIO7SbjfcoJeWbQLwCaDOXECzRMme9a3isQU/duvqjKJOcHESRal+Nhj59V/iz7fizpkzBXI1BH83Cq1NrHVXaFdGD9KKwK9DXWxZHsNxPwkEu7oQareRBusHJhemcSJYDfjBhRDe8mqyR/FpPsG9KUzUWvclUUAAAAASUVORK5CYII=",
|
||||
"@b64:icon32.png": "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAABAFJREFUWEe9V1tPE0EUPkMtqIB4e+HFd/UXEOKjD8ZLDN64CYIIxVvEK8RL1CiVggKCSLwQIYBG0QcTHzT6aPgJ/gWNiAUKpS3tmHNmZzu7O7sWQfrQZna3853zne98Z5aB7dM0xHlsLgkAHDgH4JwDx2fwl9Z0B7/Me/iMdq3sgfdX5eTD4DnGVEhzcXaA8+RCEniK0JYdHIOWCWVl+WH0Si5h09dKggvyOLAsP7xqzhN0nHqywFcicwkumXhzbSNj/7PmKu12cEzcl50P7GT/giDFQ3BPT+eYuqntjmoFN3gh13ymMjRDNMs9deAkbg4YQEKI2EPtT88oAXRFHepPcg7DF/MogMWAIyZrfIwBuLeaCm5vWVxXtEdg5JIAd/uUBcOQTFEzi+CNhKkLAn1xXGv7PMcP0BtIZ++J4nGztDUMKQK2ghMDDX1xuqozmWdnV5vbjn1NwMFiP62r789SjYcuWDMvb5uG0SvrzP8caQ0TqC5zyTpreBQzNOB0uOdGADWdc+SGL5rW0maB3lnoP50WHV5DcNzoZXOBGcDhu789wYmB+l4jAI29xhIcsn0kepGJYSL2zCViWXCKmHnVsh4yAcf92ImemJBgBt5evG0V1O9Ka6IihFmDhfbAwymYnCE/19bcngyr65kX+EYnuA0Wtc9J/Qa4fP5lS5p6vH/ozqRF7Y79kasUAKt7OG+2oQ588Ly11ovthJJbv8Rws5URwWkmHO+Okk7t4AW5AN31QnTy8+5rDEqKvdty/FscirZmW/73fjwKAx8jpoYkOGmgFgNwmeeS9sbeCMzMkbkC+Ymmn1Vzwf2qd+bCviLRxvtv/tSCUxfUdNHOTg3YvHz4cr7Zavi8WnNUvDSZsesbCbTk1gTRnkpxmvkUvkG7isdqOuecGtCA61pNXpOCQ5ix65tM+r0yN4gEduzBrFUDmikW2L0GdmwXLnjobhiymGDs9dUNdA39ojw4CW9vLA4cS8mqMQDFZNxGqM7hMIADtyeJ4Pk4hw93NlNAZa0TEI0JzehoNzIW47jqfsTwIef8Hrmc9vXFtp/6/N5rPxSNKSLGNqzqiJAF6TJXB8tSAthz9bvhA1ZwYuBox4x5ILGfXFTalxLAbgxAbV8jYdJAZTsGkB40mRyjHCNWzpIMai7ZJi/BA0lFaNpVA3Zzcaxd7NXqK07aJTgZUUXbtKsGPA8T6hvSP2RuHkqruziPz4vDhNfp1RLMMmSOcL7sPPFmVBoMO8uQ4Tz/W5/bay4zxzJ8ebBFvBmVh2Z5ciHunN/K6dVtnjsPM941l90APj98aS9Mv6mW3otQEJmeZJaSOfP54XN7YfrlVO3x0lCCJ6QmZBmWseafghssr+d/AJMFI9vo7StdAAAAAElFTkSuQmCC",
|
||||
"@b64:icon48.png": "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAB0dJREFUaEPVWktslUUUPmMfgJRH0cSVGF2IMcTEmMjKFQtNIOEhSIW2vBSRgghYykup8ii0thZRBKoJj5aXlZWJKyAuXLnTuNGVCxJE7PtJ2zvm/DNn5sz8//z33lIB76Lc+z/Pd+ab73xnBgEpn6rTYx0gZamUABKiP9FH4gH8N/pOf/R3daU6ru+h77Hj6mHx693jnRdqps8KhSmSTmw+NcrikjoY9jIdvroo5TiC0bgJtMmDDlz9psyo5Bjsgm5W11zcNTMWr3NgS4uUGZkxWYmyk5B1ylr0cjYaznGWRTNqNIgUJB9NlggOyoweJUsIuLR7honbfMHgpczYgB4sbfTQEkXVCFAyQQBc3q1GwwB4WGlDNDWzTo/ilb2lFkDV6bEInuLew0cbmiMOnQDg232zRISi6tQoxf4g1SadNoxCNNHbEQBKpczI0ohlesIaaUtRm5bNkxwBW39sUAsSUxQ+kSXA2R1TnXt++PkutN0YBumpTYg2RjBIpkB0ik2Y/Txp4wePUa07NmhrREBtzu0ocQCU1/caCfXVJkQbn0Zi00nU/NyLFE7701Vu9tOKIZ6rbOyHc172s92D58vquhwZN4lnbBHvfjWiJ7iuiFmKVMvmybm827mmsrEP/Ozn8pAVhxGACc9Wdl0/8EwEIFiMvCLVsiX/4HMJNHTNG4c6NS2VLYkVVZkAIORt6iqL4fHpic7jXmJMvRcBJNGG2w2x8cRdk+eQtxkaBji/w81+RiK3B6D1g0dNEKub+rV/kzAyag1da/U0KCxQly071AOTCgFefq4Iti6aYu5FvtNUzEYbQoXxindO3GVFLNnbfJ1AnbXNAzA6CnCeAVATtg9ObJoKJVPio5XJAJQ39ER0uFAzI5b9FYc7GU3CtOF0igBks8R4/pv37AisbUbJVMq1ccEkmPesTi8AnLs2DJXzk1Vqf2s//HFzzIzSxV0uCJq02Wijyoua3GLDl8PGkJPhMwj1k/DaDa8Vw7w5BYCZ9y3x2e1T4cYvI/D87EJ4Iu54nUz/+OsInPx+QBtoCZd2z4zO+xOW0ykGSHAAXwyHVUhBJbeuUOdYpLLNbKvxXF1yow3VrUhGN0QA9I3j6aSQNttdi0DBr6pHvgO07ZyeiAcpQ3TIhzbcdIq3jw/FVSjHTsr3NhTl7zfH4OM2q0j4Ap/vHNHyg6j3dnS5TBo6M9qYhicDIN46PuSqUA60qX59Msx9yk5cHszKoz2AHjfJ28ybUwTbliaP1tBdCeVHUYXcymuKFwNoAKCMIgB6mWoRA3QCgK2LiuHFZwqz0fuezy/95J/EBYQk0ynWfz5ouuhQA35mmy1W9xxdHg9YXHvH9AicNkottYyuPzaokx5WmzOBSYqxoCVu3Tktj7DspcsOdED7h8EVE1hc+zdTPbu0YyYxqlAEIIU2JKOo9fxT0YBeXlGOPJJtREwHCwIkoO3gxYeswO2uDJ2A9o8ec56/pPZOaotrCtm65gFdiMOdFI4N2oDW6hJYUdcLRTgNcL4EOqmRMQnt+1SBwk+oSL2/pASarvZqvkt44ekiqK1U1dlmXwLgSg+jDXcOYm3zgB6RcJHSwsTWiwBefakIKuYre7HyaLdeoVPDTNWVA/A7qYt7SqGowPolzLiRD1Msk2njqBACcFSIyWjoOLpLRzqP9DjrNm01M6DgEXdakNaTLCZxP1faOADWfNZvO8ocl/vaquOVtayu2ylGl/dYCiGU5Qc6TNP/ncd3Qxk1BEphUmhDaonDLtY09SerEFu4pTLPG+0LNS4IsgVUOa/sLXWGYPvJHvjz9kh0zAfw02/D0HAFR5EB8D0XWykkiYj6AR9AVjppYD4AfDf3Nq/MLYYti6xyoWQiV5Ozfye4oOYuCrt2A98pVjf2sZiZEqWsEkdDKAEXWU2WkxpwYwPYs67ud+UyV7XhtOHGT1Q29lkVSqFNyFG6U3Xifi3c95edD2bVR1tvRqcIQC60eRgAJNFJVHzaa8pm2uZCUg87cfmOP2nh3ltmc4TvO5BIkJ2IAFBlS1IbuiHNz/8XQBbsuWXaTseieHQSFQ29jowaOpn54G0uJKwS82UOVQftNhH91go5brXhxUt5MO1Gy+t77BQY7yox2wdLa8DzLVL+s7gLVb5DglhV36MrcUInxDbgfC8T/51fJ+UEEyhSftZjy4tYyBDA/402hpYaQIeUspTQ2glj6rrxMMaVjrMBD1niUJGKHSejaXcDOiM/++aRbjWI6g3Jmw5syYVXWA7KHA804BNFG3rOtabZao8sAqADpyB8vsUm531WG04bUqHrBABPlh3u0rHHDZNfqfNZt5lw2miJxuAjM0dFqKyuW290K+5bmvi/H4zaOPQTAq43PukCiOxwXRf+XwMtsbwYubsjlk4Jy/FeA8+LTqgYBY9TwWJ1BlfNKHhnBLgdWH4QhSmuQve7SPkqRLThsabuGS070BFJbL7LfROqNiA7rzXODi4e/QutLYGIHeCyTgAAAABJRU5ErkJggg==",
|
||||
|
||||
+22
-5
@@ -4,6 +4,7 @@
|
||||
import { ensureSchema, getSetting, setSetting } from './db.js';
|
||||
import { hashPassword, verifyPassword, encryptJSON, decryptJSON, decryptJSONWith, accountKey, randomHex } from './crypto.js';
|
||||
import { handleExtZip } from './ext-zip.js';
|
||||
import { EXT_FILES } from './ext-files.js';
|
||||
import { runAll, runAccount } from './runner.js';
|
||||
import { getSite, siteMeta, getBrowserScript } from './sites/index.js';
|
||||
import { listCommunitySites, importSiteConfig, deleteCommunitySite, validateSiteConfig, makeCommunitySite, exportAccountConfig } from './community.js';
|
||||
@@ -222,8 +223,13 @@ async function handleApi(req, env, url) {
|
||||
const contentLength = Number(req.headers.get('Content-Length') || '0') || 0;
|
||||
if (contentLength > MAX_BODY_BYTES) return json({ error: '请求体过大(上限 512KB)' }, 413);
|
||||
|
||||
// 跨站写操作兜底:带了会话 Cookie 的浏览器请求,Origin 必须是本面板
|
||||
if (method !== 'GET' && method !== 'HEAD' && parseCookies(req).sid && !sameOrigin(req, url)) {
|
||||
// 跨站写操作兜底:带了会话 Cookie 的浏览器请求,Origin 必须是本面板。
|
||||
// 【2026-09-29 修】/api/external/* 走 API Key 鉴权(自定义请求头,CSRF 伪造不了),
|
||||
// 不适用这条会话 CSRF 检查:扩展回传中继结果时,浏览器会自动带上用户登录面板的
|
||||
// sid 会话 Cookie,而扩展的 fetch 没有 Origin 头 —— 以前这里直接 403,
|
||||
// 导致「中继结果回传失败(HTTP 403)」,签到了却写不回结果。
|
||||
if (method !== 'GET' && method !== 'HEAD' && !path.startsWith('/api/external/')
|
||||
&& parseCookies(req).sid && !sameOrigin(req, url)) {
|
||||
return json({ error: '跨站请求被拒绝(Origin 与面板不一致)' }, 403);
|
||||
}
|
||||
|
||||
@@ -425,11 +431,20 @@ async function handleApi(req, env, url) {
|
||||
if (path === '/api/external/ping' && method === 'GET') {
|
||||
const deny = await extGuard();
|
||||
if (deny) return deny;
|
||||
// 版本号取自 src/version.js(唯一真源)—— 以前这里写死 '2.2',
|
||||
// 扩展「面板连接检查」永远显示旧版本,升级了面板也看不出来。
|
||||
return json({ ok: true, version: PANEL_VERSION, time: Date.now() });
|
||||
}
|
||||
|
||||
// ---- 扩展最新版本号(弹窗里的更新按钮用:比对本地版本,有新版就提示下载)----
|
||||
// 不需要 API Key:版本号不是敏感信息
|
||||
if (path === '/api/external/ext-version' && method === 'GET') {
|
||||
let ver = '';
|
||||
try {
|
||||
const m = JSON.parse(EXT_FILES['manifest.json'] || '{}');
|
||||
ver = String(m.version || '');
|
||||
} catch { /* 忽略 */ }
|
||||
return json({ ok: true, version: ver });
|
||||
}
|
||||
|
||||
// ---- 浏览器扩展:任务已并入「本地网络中继」,这里不再下发脚本 ----
|
||||
// 浏览器模式(含站点默认 browser 的吾爱破解 / NodeSeek / V2EX / 看雪 / Discuz)统一走中继:
|
||||
// 站点逻辑仍在 Worker,HTTP 请求由扩展在用户本地网络中执行(见 runner.js),
|
||||
@@ -1377,7 +1392,8 @@ async function handleApi(req, env, url) {
|
||||
const version = (await getSetting(env.DB, 'relay_version')) || '';
|
||||
const { relayBacklog } = await import('./lib/relay.js');
|
||||
const backlog = await relayBacklog(env.DB);
|
||||
return json({ online, last_poll: last, version, backlog });
|
||||
const latestVersion = (() => { try { return String(JSON.parse(EXT_FILES['manifest.json'] || '{}').version || ''); } catch { return ''; } })();
|
||||
return json({ online, last_poll: last, version, latest_version: latestVersion, backlog });
|
||||
}
|
||||
|
||||
// 修改管理密码
|
||||
@@ -1412,6 +1428,7 @@ async function handleApi(req, env, url) {
|
||||
online: !!lastSeen && Date.now() - lastSeen < 120000,
|
||||
last_seen_ago_sec: lastSeen ? Math.floor((Date.now() - lastSeen) / 1000) : null,
|
||||
version: info.version || '',
|
||||
latest_version: (() => { try { return String(JSON.parse(EXT_FILES['manifest.json'] || '{}').version || ''); } catch { return ''; } })(),
|
||||
ua: info.ua || '',
|
||||
capabilities: info.capabilities || [],
|
||||
signed_recently: !!lastSigned && Date.now() - lastSigned < 600000,
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@
|
||||
// · 测试会断言 package.json 的 version 和它一致(改了这里忘了改那边会直接报红)
|
||||
// 扩展的版本在 public/ext-src/manifest.json 里(它有自己的发布节奏:改过扩展代码,
|
||||
// 用户就得重新下载 + 在 chrome://extensions 点一次「重新加载」,所以必须单独能看出来)。
|
||||
export const PANEL_VERSION = '2.11.0';
|
||||
export const PANEL_VERSION = '2.13.0';
|
||||
Reference in new issue
Block a user