修复扩展弹窗UI:标题行清理、按钮样式统一、Cookie计数移至域名行;修复ping接口写死版本号2.2

This commit is contained in:
hani committed 2026-09-29 22:10:54 +08:00
1 parent 16570bdc94
commit 8bb2f3cdc2
4 files changed
+40 -19

No files matched your search

+30 -15
View File
@@ -3,33 +3,48 @@
<head>
<meta charset="utf-8">
<style>
* { box-sizing: border-box; }
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; }
.header { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.header h3 { margin: 0; font-size: 15px; font-weight: 700; }
.header .ver { font-size: 12px; color: #8b98b3; font-weight: 400; }
.domain { font-size: 12px; color: #8b98b3; line-height: 1.6; margin-bottom: 12px; word-break: break-all; }
.domain .count { display: inline-block; background: rgba(91,140,255,.15); color: #7aa5ff; padding: 1px 8px; border-radius: 10px; font-size: 11px; margin-left: 4px; }
button { width: 100%; padding: 11px; margin: 0 0 8px; border: 0; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
.primary { background: #5b8cff; color: #fff; }
.primary:hover { background: #4a7de8; }
.secondary { background: transparent; border: 1px solid #26314a; color: #e8edf7; }
.secondary:hover { border-color: #5b8cff; }
button:disabled { opacity: .5; cursor: wait; }
#status { font-size: 12px; color: #8b98b3; margin-top: 8px; min-height: 18px; }
.note { font-size: 12px; color: #8b98b3; line-height: 1.6; margin: 0 0 12px; }
.divider { border: 0; border-top: 1px solid #1c2538; margin: 4px 0 12px; }
.section-title { font-size: 13px; font-weight: 700; margin: 0 0 8px; }
label { font-size: 12px; color: #8b98b3; display: block; margin: 0 0 4px; }
input { width: 100%; background: #0d1322; border: 1px solid #26314a; color: #e8edf7; border-radius: 8px; padding: 9px 12px; font-size: 13px; margin-bottom: 10px; }
input:focus { outline: none; border-color: #5b8cff; }
#status { font-size: 12px; color: #8b98b3; margin-top: 4px; min-height: 18px; line-height: 1.5; }
#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; }
</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>
</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>
+7 -1
View File
@@ -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` : '')
+2 -2
View File
@@ -2,8 +2,8 @@
// 源文件在 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.10\"\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
"popup.html": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n<style>\n * { box-sizing: border-box; }\n body { width: 320px; padding: 16px; font-family: -apple-system, \"PingFang SC\", \"Microsoft YaHei\", sans-serif; margin: 0; background: #0f1420; color: #e8edf7; }\n .header { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }\n .header h3 { margin: 0; font-size: 15px; font-weight: 700; }\n .header .ver { font-size: 12px; color: #8b98b3; font-weight: 400; }\n .domain { font-size: 12px; color: #8b98b3; line-height: 1.6; margin-bottom: 12px; word-break: break-all; }\n .domain .count { display: inline-block; background: rgba(91,140,255,.15); color: #7aa5ff; padding: 1px 8px; border-radius: 10px; font-size: 11px; margin-left: 4px; }\n button { width: 100%; padding: 11px; margin: 0 0 8px; border: 0; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }\n .primary { background: #5b8cff; color: #fff; }\n .primary:hover { background: #4a7de8; }\n .secondary { background: transparent; border: 1px solid #26314a; color: #e8edf7; }\n .secondary:hover { border-color: #5b8cff; }\n button:disabled { opacity: .5; cursor: wait; }\n .note { font-size: 12px; color: #8b98b3; line-height: 1.6; margin: 0 0 12px; }\n .divider { border: 0; border-top: 1px solid #1c2538; margin: 4px 0 12px; }\n .section-title { font-size: 13px; font-weight: 700; margin: 0 0 8px; }\n label { font-size: 12px; color: #8b98b3; display: block; margin: 0 0 4px; }\n input { width: 100%; background: #0d1322; border: 1px solid #26314a; color: #e8edf7; border-radius: 8px; padding: 9px 12px; font-size: 13px; margin-bottom: 10px; }\n input:focus { outline: none; border-color: #5b8cff; }\n #status { font-size: 12px; color: #8b98b3; margin-top: 4px; min-height: 18px; line-height: 1.5; }\n #status.ok { color: #3ddc84; }\n #status.err { color: #ff6b6b; }\n</style>\n</head>\n<body>\n <div class=\"header\">\n <h3>🍪 签到面板助手</h3>\n <span class=\"ver\" id=\"title-ver\"></span>\n </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\">发送方式是「一次性交接码」:凭据不进地址栏,短码 5 分钟内有效、取一次即作废。</p>\n\n <hr class=\"divider\">\n <div class=\"section-title\">面板连接</div>\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 <button class=\"secondary\" id=\"btn-check-conn\">🔌 检查连接</button>\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
"@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",
+1 -1
View File
@@ -425,7 +425,7 @@ async function handleApi(req, env, url) {
if (path === '/api/external/ping' && method === 'GET') {
const deny = await extGuard();
if (deny) return deny;
return json({ ok: true, version: '2.2', time: Date.now() });
return json({ ok: true, version: PANEL_VERSION, time: Date.now() });
}
// ---- 浏览器扩展:任务已并入「本地网络中继」,这里不再下发脚本 ----