扩展v2.13:弹窗UI统一配色+跟随系统深浅色主题;新增更新检查按钮(/api/external/ext-version)

This commit is contained in:
hani committed 2026-09-29 22:19:20 +08:00
1 parent 596d50377e
commit e82a40873e
5 files changed
+151 -22

No files matched your search

+1 -1
View File
@@ -33,5 +33,5 @@
"alarms",
"tabs"
],
"version": "2.11"
"version": "2.13"
}
+67 -18
View File
@@ -4,34 +4,83 @@
<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; }
.header { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
/* 浅色主题(默认) */
: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: #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; }
.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; }
.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.secondary { background: transparent; border-color: var(--border-strong); color: var(--text); }
.secondary:hover { border-color: var(--accent); color: var(--accent-text); }
button:disabled { opacity: .5; cursor: wait; }
.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; }
.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: #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: 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>
<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>
+68
View File
@@ -286,6 +286,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 秒内接到单,不需要在弹窗上再点一次。
+3 -3
View File
@@ -1,9 +1,9 @@
// 自动生成:扩展源文件嵌入(运行 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 * { 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
"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.13\"\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; }\n .primary { background: var(--accent); border-color: var(--accent); color: #fff; }\n .primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }\n .secondary { background: transparent; 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\">发送方式是「一次性交接码」:凭据不进地址栏,短码 5 分钟内有效、取一次Line 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",
+12
View File
@@ -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';
@@ -428,6 +429,17 @@ async function handleApi(req, env, url) {
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),