Files
daily-checkin-panel/src/ext-files.js
T

13 lines
76 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 自动生成:扩展源文件嵌入(运行 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插件2.2:一键取 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 \"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\": \"cookie插件2.2\",\n \"permissions\": [\n \"cookies\",\n \"activeTab\",\n \"scripting\",\n \"storage\",\n \"alarms\",\n \"tabs\"\n ],\n \"version\": \"2.3\"\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>🖊️ cookie插件2.2 <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 <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://daily-checkin-panel.guo527029137.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// 采集当前标签页的 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.sort((a, b) =>\n (b.path || '').length - (a.path || '').length ||\n String(a.domain).localeCompare(String(b.domain)) ||\n String(a.name).localeCompare(String(b.name)));\n return list;\n}\n\n// Cookie 概况:Cookie 数 / 域数 / HttpOnly 数 / 会话与持久数 / 最近过期时间\nfunction describeCookies(list) {\n const domains = new Set();\n let httpOnly = 0;\n let session = 0;\n let persistent = 0;\n for (const c of list) {\n domains.add(c.domain || '');\n if (c.httpOnly) httpOnly++;\n if (c.session) session++; else persistent++;\n }\n return { count: list.length, domains: domains.size, httpOnly, session, persistent };\n}\n\nasync function init() {\n // 读取保存的面板地址和 API Key;没有保存过则用下载时注入的默认地址(面板动态生成 zip 时填入)\n const { panelUrl, apiKey } = await chrome.storage.sync.get(['panelUrl', 'apiKey']);\n const hasDefault = typeof DEFAULT_PANEL_URL !== 'undefined' && DEFAULT_PANEL_URL && DEFAULT_PANEL_URL.startsWith('http');\n if (panelUrl) {\n $('panel-url').value = panelUrl;\n } else if (hasDefault) {\n $('panel-url').value = DEFAULT_PANEL_URL;\n // 自动保存默认地址,避免下次为空\n chrome.storage.sync.set({ panelUrl: DEFAULT_PANEL_URL }).catch(() => {});\n }\n if (apiKey) $('api-key').value = apiKey;\n\n // 获取当前标签页\n const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });\n if (!tab || !tab.url || !tab.url.startsWith('http')) {\n status('请在目标网站页面使用', 'err');\n return;\n }\n const url = new URL(tab.url);\n domain = url.hostname;\n $('domain').textContent = '当前网站:' + domain;\n\n // 获取页面真实的 User-Agent(吾爱等站点要求 UA 与 Cookie 配对)\n let pageUA = '';\n try {\n const [{ result }] = await chrome.scripting.executeScript({\n target: { tabId: tab.id },\n func: () => navigator.userAgent,\n });\n pageUA = result || '';\n } catch { /* 忽略,用默认 */ }\n\n // 精准而全面地抓取 Cookie(与浏览器实际发送的登录态一致)\n cookies = await collectCookies(tab);\n const stat = describeCookies(cookies);\n\n // 顺带抓 localStorage(部分站点把 token 放这里,如 akile)\n let localStore = {};\n try {\n const [{ result }] = await chrome.scripting.executeScript({\n target: { tabId: tab.id },\n func: () => {\n const o = {};\n try { for (let i = 0; i < localStorage.length && i < 200; i++) { const k = localStorage.key(i); if (k) o[k] = String(localStorage.getItem(k) || '').slice(0, 2000); } } catch { /* 忽略 */ }\n return o;\n },\n });\n localStore = result || {};\n } catch { /* 忽略 */ }\n\n window._pageUA = pageUA;\n window._localStore = localStore;\n const lsCount = Object.keys(localStore).length;\n $('count').textContent = cookies.length + ' 个';\n status(\n cookies.length\n ? `已读取 ${cookies.length} 个 Cookie(${stat.domains} 个域 / ${stat.httpOnly} 个 HttpOnly)` + (lsCount ? ` + ${lsCount} 项 localStorage` : '')\n : '该网站没有 Cookie',\n cookies.length ? 'ok' : 'err'\n );\n}\n\nfunction cookieString() {\n return cookies.map((c) => `${c.name}=${c.value}`).join('; ');\n}\n\nfunction fullPayload() {\n return JSON.stringify({\n domain,\n userAgent: window._pageUA || '',\n cookies: cookieString(),\n // 附带可读的分解信息,面板据此清晰展示:每个 Cookie 的名称/域/是否 HttpOnly\n cookieList: cookies.map((c) => ({\n name: c.name,\n value: c.value,\n domain: c.domain,\n path: c.path,\n secure: !!c.secure,\n httpOnly: !!c.httpOnly,\n sameSite: c.sameSite || '',\n session: !!c.session,\n expirationDate: c.expirationDate || null,\n })),\n localStorage: window._localStore || {},\n stats: describeCookies(cookies),\n ts: Date.now(),\n });\n}\n\n$('btn-copy').onclick = async () => {\n if (!cookies.length) return status('没有可复制的 Cookie', 'err');\n // 复制完整 JSON:面板粘贴后自动解析出域名、UA、Cookie\n await navigator.clipboard.writeText(fullPayload());\n status(`已复制 ${cookies.length} 个 Cookie + UA,去面板「粘贴扩展内容」粘贴即可`, 'ok');\n};\n\n$('btn-send').onclick = async () => {\n if (!cookies.length) return status('没有可发送的 Cookie', 'err');\n let panelUrl = $('panel-url').value.trim().replace(/\\/$/, '');\n if (!panelUrl) return status('请先填写签到面板地址', 'err');\n if (!panelUrl.startsWith('http')) panelUrl = 'https://' + panelUrl;\n await chrome.storage.sync.set({ panelUrl });\n\n // 把 Cookie + UA + 域名编码进 URL hash,面板 JS 读取后自动弹出确认框\n // 用 base64url 编码(+/= 替换为 -_.),避免特殊字符在地址栏被转义或截断\n const b64 = btoa(unescape(encodeURIComponent(fullPayload())));\n const payload = b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n chrome.tabs.create({ url: panelUrl + '#ext-cookies=' + payload });\n status('已打开面板,请在面板中确认保存', 'ok');\n};\n\n$('panel-url').oninput = () => {\n chrome.storage.sync.set({ panelUrl: $('panel-url').value.trim().replace(/\\/$/, '') });\n};\n\n$('api-key').oninput = () => {\n chrome.storage.sync.set({ apiKey: $('api-key').value.trim() });\n};\n\n// 兼容旧的 onchange(保留)\n$('panel-url').onchange = () => {\n chrome.storage.sync.set({ panelUrl: $('panel-url').value.trim().replace(/\\/$/, '') });\n};\n\n$('api-key').onchange = () => {\n chrome.storage.sync.set({ apiKey: $('api-key').value.trim() });\n};\n\n// 打开弹窗时顺手叫醒一次中继(后台常驻长轮询,本来就会自己跑)。\n// 以前这里还有个「立即执行待办签到」按钮——它会挂住弹窗、也让用户以为要手动点才干活,已删掉:\n// 面板点「执行」时后台会在 1 秒内接到单,不需要在弹窗上再点一次。\n// 延到下一个 tick:先把弹窗自己的 UI 初始化完,再去叫醒后台,\n// 不抢在主界面之前干活(后台不在时静默忽略,不影响取 Cookie)。\nsetTimeout(() => {\n try {\n const p = chrome.runtime.sendMessage({ action: 'runRelayNow' });\n if (p && typeof p.catch === 'function') p.catch(() => {});\n } catch { /* 忽略 */ }\n}, 0);\n\n// 原生页面巡检:不依赖 eval / new Function(MV3 禁止),用于读取网站真实反馈。\nasync function inspectPageInPopup(tabId) {\n const results = await chrome.scripting.executeScript({\n target: { tabId },\n func: () => {\n const title = document.title || '';\n const bodyText = (document.body && document.body.innerText) || '';\n const html = document.documentElement ? document.documentElement.innerHTML : '';\n const all = bodyText + '\\n' + html;\n const compact = bodyText.replace(/\\s+/g, ' ').trim();\n // 1) 人机验证 / 验证码 / WAF:优先判定——这些页面里往往也夹带「签到」字样\n const CAPTCHA = ['验证码', '人机验证', '安全验证', '请完成验证', '滑动验证', '滑块', '智能验证', 'slidercaptcha', 'geetest', '极验', 'recaptcha', 'hcaptcha', 'turnstile', 'cf_chl', 'challenge-platform', 'Just a moment', 'checking your browser', 'turing.captcha.qcloud.com', 'TCaptcha', 'CaptchaAId', 'ibex'];\n if (CAPTCHA.some((m) => all.includes(m))) return { ok: false, message: '遇到人机验证/安全验证(验证码/滑块),需人工验证后重试|页面:' + title };\n const WAF = ['waf_zw_verify', 'WZWS_CONFIRM_PREFIX_LABEL', 'Access Denied', '403 Forbidden', '请求被拦截'];\n if (WAF.some((m) => all.includes(m))) return { ok: false, message: '遇到网站安全防护(WAF),请在浏览器完成验证后重试|页面:' + title };\n // ---- 找「去签到」入口(Discuz 系论坛通用)----\n const ALREADY = /已签到|已经签到|已打卡|签到完毕|签到完成|连续签到|今日已签|明天再来|下期再来/;\n const SIGN_TEXT = /打卡签到|立即签到|点击签到|点击打卡|签到领奖|每日签到|每日打卡|签个到|去签到|未打卡|还没有签到|今天还没有/;\n const SIGN_IMG = /qds\\.png|signin_no\\.png|pperwb\\.gif|wb\\.png|qiandao|qdbg|sign_?in/i;\n const SIGN_SEL = '#kx,#JD_sign,#addsign,#sg_sign,#dcsignin_tips,#tt_sign,.punch_btn,.go-user-qiandao,a.initiate-checkin,#my_amupper,#pper_a,#sign_title,#setsign,.click-qiandao,.zzhuti_qd_1,.user-index-qd,.taskbtn';\n const attrOf = (el) => ((el.getAttribute && el.getAttribute('href') || '') + ' ' + (el.id || '') + ' ' +\n (el.className || '') + ' ' + (el.getAttribute && el.getAttribute('title') || '')).replace(/\\s+/g, ' ');\n const textOf = (el) => String(el.innerText || el.textContent || '').replace(/\\s+/g, '');\n const imgsOf = (el) => Array.from(el.querySelectorAll ? el.querySelectorAll('img') : [])\n .concat(el.tagName === 'IMG' ? [el] : [])\n .map((i) => (i.getAttribute('src') || '') + ' ' + (i.getAttribute('alt') || '')).join(' ');\n const findSignLink = () => {\n const applies = Array.from(document.querySelectorAll('a[href*=\"do=apply\"], a[href*=\"do=draw\"]'))\n .filter((a) => /mod=task/i.test(a.getAttribute('href') || ''));\n for (const a of applies) {\n if (ALREADY.test(textOf(a))) continue;\n const own = textOf(a) + ' ' + attrOf(a) + ' ' + imgsOf(a);\n const row = a.closest('tr') || a.closest('li') || a.closest('table') || a.parentElement;\n const rowTxt = ((row && row.innerText) || '').replace(/\\s+/g, ' ').slice(0, 200);\n const rowDone = /已完成|已申请|已领取|已打卡/.test(rowTxt) && !/立即申请|马上申请|申请任务/.test(rowTxt);\n if (rowDone) return { href: a.getAttribute('href') || '', label: (textOf(a) || '签到'), done: true };\n if (/qds\\.png|打卡|每日|签到/i.test(own + ' ' + rowTxt) || applies.length === 1) {\n return { href: a.getAttribute('href') || '', label: (textOf(a) || '打卡签到'), done: false };\n }\n }\n const known = Array.from(document.querySelectorAll(SIGN_SEL));\n for (const el of known) {\n const own = textOf(el) + ' ' + attrOf(el) + ' ' + imgsOf(el);\n if (ALREADY.test(own)) continue;\n const href = (el.getAttribute && el.getAttribute('href')) || '';\n const usable = href && !/^javascript:void/i.test(href) ? href : '';\n el.setAttribute('data-panel-sign', '1');\n return { href: usable, clickSel: '[data-panel-sign=\"1\"]', label: (textOf(el) || '签到'), done: false };\n }\n for (const el of Array.from(document.querySelectorAll('a,button,[onclick],img'))) {\n const t = textOf(el);\n const im = imgsOf(el);\n if (ALREADY.test(t + ' ' + im)) continue;\n if (!(SIGN_TEXT.test(t) || SIGN_IMG.test(im))) continue;\n const href = (el.getAttribute && el.getAttribute('href')) || '';\n if (!href && !el.onclick && !el.getAttribute('onclick')) continue;\n el.setAttribute('data-panel-sign', '1');\n return { href: /^javascript:/i.test(href) ? '' : href, clickSel: '[data-panel-sign=\"1\"]', label: (t || '签到'), done: false };\n }\n return null;\n };\n const SIGNED = /签到完毕|签到完成|您已完成过此任务|您已经完成此任务|您今日已经签到|您今天已经签到|今天已经签到|已经签到过|已经完成签到|今日任务已完成|今日已签到|今天已完成签到|已连续签到|下期再来|明天再来|无需重复|恭喜.{0,14}(完成|获得|领到|签到)/;\n const SUCCESS = /任务已完成|签到成功|打卡成功|领取成功|成功领取|获得.{0,10}(鸡腿|积分|金币|铜币|银币|吾爱币|AK|币|空间|MB|GB)/;\n let link = null;\n try { link = findSignLink(); } catch (e) { link = null; }\n if (!link) {\n const am = all.match(/<a[^>]*href=[\"']([^\"']*mod=task[^\"']*do=(?:apply|draw)[^\"']*)[\"'][^>]*>/i);\n if (am && /qds\\.png|打卡签到|每日签到|每日打卡|签到完毕/i.test(all) && !/已完成过此任务|您已经完成此任务/.test(all)) {\n link = { href: am[1].replace(/&amp;/g, '&'), label: '打卡签到', done: false };\n }\n }\n if (SUCCESS.test(all)) return { ok: true, message: '签到成功|' + (compact.slice(0, 120) || title) };\n if (SIGNED.test(all)) return { ok: true, message: '今日已签到,无需重复' };\n if (link && link.done) return { ok: true, message: '今日已签到(任务页显示:已完成)' };\n // 4) 未登录(Discuz 游客页会写「注册[Register]」)——比「未签到」更优先:\n // 登录失效时页面导航里也有「每日签到」这类字样。\n if (/需要先登录|请先登录|还未登录|请登录后|登录已失效|登录失效|请重新登录|未登录|注册\\[Register\\]|立即注册|登录后才能/.test(all)) return { ok: false, message: '登录已失效,请重新获取 Cookie' };\n if (!link && /打卡签到|立即签到|点击打卡|每日签到|每日打卡|还没有签到|未打卡/.test(all)) {\n return { ok: false, unsigned: true, href: '', clickSel: '', message: '网站显示还没签到,但页面上找不到可点的签到入口' };\n }\n if (link) return { ok: false, unsigned: true, href: link.href || '', clickSel: link.clickSel || '', message: '网站还挂着「' + (link.label || '签到') + '」入口 —— 今天还没签上' };\n return { ok: false, message: '未识别到成功标识|页面:' + title + '|' + compact.slice(0, 140) };\n },\n });\n return (results && results[0] && results[0].result) || null;\n}\n\n// 尝试执行面板下发的脚本字符串(MV3 下通常失效,失败时回退原生巡检)。\nasync function runPanelScriptInPopup(tabId, job) {\n const execPromise = chrome.scripting.executeScript({\n target: { tabId },\n func: (scriptStr, params) => {\n const fn = new Function('params', `return (${scriptStr})(params)`);\n return fn(params);\n },\n args: [job.script, job.params || {}],\n });\n const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('脚本执行超时(60秒)')), 60000));\n const results = await Promise.race([execPromise, timeoutPromise]);\n return (results && results[0] && results[0].result) || null;\n}\n\n// 在弹窗上下文中执行单个签到任务(复用目标域名标签页,携带用户 Cookie)\n// onStep: 步骤回调,用于显示详细进度\nasync function executeJobInPopup(job, onStep) {\n const startMs = Date.now();\n const step = (msg) => { if (onStep) onStep(msg); };\n let tab = null;\n let created = false;\n try {\n if (!job.domain) throw new Error('任务缺少目标域名');\n step('查找标签页…');\n const tabs = await chrome.tabs.query({ url: `*://${job.domain}/*` });\n if (tabs.length > 0) {\n tab = tabs[0];\n step('复用已有标签页…');\n } else {\n step('打开新标签页…');\n tab = await chrome.tabs.create({ url: `https://${job.domain}/`, active: false });\n created = true;\n step('等待页面加载…');\n await new Promise((resolve) => {\n const listener = (tabId, info) => {\n if (tabId === tab.id && info.status === 'complete') {\n chrome.tabs.onUpdated.removeListener(listener);\n resolve();\n }\n };\n chrome.tabs.onUpdated.addListener(listener);\n setTimeout(resolve, 15000);\n });\n }\n // 如果任务指定了导航 URL(如吾爱破解),先导航到该页面(模拟手动点击),再执行检查脚本\n if (job.navigate_url) {\n step('导航到签到页面…');\n await chrome.tabs.update(tab.id, { url: job.navigate_url });\n step('等待签到页面加载…');\n await new Promise((resolve) => {\n const listener = (tabId, info) => {\n if (tabId === tab.id && info.status === 'complete') {\n chrome.tabs.onUpdated.removeListener(listener);\n resolve();\n }\n };\n chrome.tabs.onUpdated.addListener(listener);\n setTimeout(resolve, 20000);\n });\n // 多等 2 秒,让页面内跳转完成\n await new Promise((r) => setTimeout(r, 2000));\n }\n // MV3 禁止 new Function / eval:有 navigate_url 直接原生巡检,否则先试面板脚本再回退。\n step('读取页面结果…');\n let result = null;\n if (!job.navigate_url && job.script) {\n try { result = await runPanelScriptInPopup(tab.id, job); } catch { result = null; }\n }\n if (!result) result = await inspectPageInPopup(tab.id);\n if (!result) throw new Error('无法读取页面内容(标签页可能已关闭或被重定向)');\n\n // 页面还挂着「打卡签到」入口 = 今天还没签:链接→导航过去,JS 按钮→在页面里点一下\n if (!result.ok && result.unsigned && (result.href || result.clickSel)) {\n try {\n let navigated = false;\n if (result.href) {\n const base = tab.url ? new URL(tab.url) : null;\n const u = new URL(result.href, tab.url || undefined);\n if ((u.protocol === 'http:' || u.protocol === 'https:') && (!base || u.host === base.host)) {\n step('网站显示还没签到,正在执行签到…');\n await chrome.tabs.update(tab.id, { url: u.href });\n await new Promise((resolve) => {\n const listener = (tabId, info) => {\n if (tabId === tab.id && info.status === 'complete') {\n chrome.tabs.onUpdated.removeListener(listener);\n resolve();\n }\n };\n chrome.tabs.onUpdated.addListener(listener);\n setTimeout(resolve, 20000);\n });\n await new Promise((r) => setTimeout(r, 2000));\n navigated = true;\n }\n }\n if (!navigated && result.clickSel) {\n step('网站显示还没签到,正在点击签到按钮…');\n await chrome.scripting.executeScript({\n target: { tabId: tab.id },\n func: (sel) => { const el = document.querySelector(sel); if (el) el.click(); },\n args: [result.clickSel],\n });\n await new Promise((r) => setTimeout(r, 3000));\n }\n const after = await inspectPageInPopup(tab.id);\n if (after) result = after;\n } catch (e) {\n console.log('[签到面板] 触发签到失败:', e.message || e);\n }\n }\n\n step('解析结果…');\n return {\n status: result.ok ? 'ok' : 'fail',\n message: String(result.message || (result.ok ? '签到成功' : '签到失败')),\n durationMs: Date.now() - startMs,\n };\n } catch (e) {\n return { status: 'fail', message: '浏览器执行失败:' + (e.message || e), durationMs: Date.now() - startMs };\n } finally {\n // 只关闭自己新建的标签页(复用的保留)\n if (created && tab && tab.id) {\n chrome.tabs.remove(tab.id).catch(() => {});\n }\n }\n}\n\n// 面板连接检查:验证面板地址和 API Key 是否可用\n$('btn-check-conn').onclick = async () => {\n status('正在检查面板连接…', '');\n try {\n let panelUrl = $('panel-url').value.trim().replace(/\\/$/, '');\n if (!panelUrl) return status('请先填写签到面板地址', 'err');\n if (!panelUrl.startsWith('http')) panelUrl = 'https://' + panelUrl;\n const apiKey = $('api-key').value.trim();\n if (!apiKey) return status('请先填写 API Key(面板设置页获取)', 'err');\n\n // ① 检查面板是否可访问(用专用 ping 接口,不消费任务)\n let resp;\n try {\n resp = await fetch(panelUrl + '/api/external/ping', {\n headers: { 'X-Api-Key': apiKey },\n });\n } catch (e) {\n return status('连接失败:面板地址无法访问(' + (e.message || '网络错误') + ')', 'err');\n }\n // ② 检查 API Key 是否有效\n if (resp.status === 401) return status('连接失败:API Key 无效,请去面板设置页重新生成', 'err');\n if (!resp.ok) return status('连接失败:面板返回 HTTP ' + resp.status, 'err');\n // ③ 解析任务列表\n let data;\n try { data = await resp.json(); } catch { return status('连接失败:面板返回数据格式错误', 'err'); }\n status(`连接正常 ✅ API Key 有效(面板版本 ${data.version || '未知'})`, 'ok');\n } catch (e) {\n status('检查失败:' + (e.message || e), 'err');\n }\n};\n\ninit();\n",
"background.js": "// 签到面板 Cookie 助手 - 后台执行引擎\n// 每小时从面板获取 browser 模式的待执行任务,在用户浏览器中完成签到(使用用户网络),上报结果。\n\n// DEFAULT_PANEL_URL 由面板在用户下载时动态注入(替换 https://daily-checkin-panel.guo527029137.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_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\n// 获取面板地址和 API Key\nasync function getConfig() {\n const { panelUrl, apiKey } = await chrome.storage.sync.get(['panelUrl', 'apiKey']);\n // 面板地址:已保存 > 下载时注入的默认值\n let url = (panelUrl || '').trim().replace(/\\/$/, '');\n if (!url && typeof DEFAULT_PANEL_URL !== 'undefined' && DEFAULT_PANEL_URL && DEFAULT_PANEL_URL.startsWith('http')) {\n url = DEFAULT_PANEL_URL.replace(/\\/$/, '');\n }\n return { panelUrl: url, apiKey: (apiKey || '').trim() };\n}\n\n// 从面板获取待执行任务\nasync function fetchJobs(panelUrl, apiKey) {\n const resp = await fetch(panelUrl + '/api/external/browser-jobs', {\n headers: { 'X-Api-Key': apiKey },\n });\n if (!resp.ok) throw new Error('获取任务失败:HTTP ' + resp.status);\n const data = await resp.json();\n return data.jobs || [];\n}\n\n// 上报结果到面板\nasync function reportResult(panelUrl, apiKey, accountId, status, message, durationMs) {\n await fetch(panelUrl + '/api/external/report', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Api-Key': apiKey },\n body: JSON.stringify({\n account_id: accountId,\n status,\n message,\n detail: '',\n duration_ms: durationMs || 0,\n }),\n }).catch(() => { /* 上报失败不阻塞 */ });\n}\n\n// 原生页面巡检:不依赖 eval / new Function(MV3 已禁止),用于读取网站真实反馈。\n// 返回 { ok, message, unsigned?, href? } 或 null。\n//\n// 判定顺序很关键(都是踩过的坑):\n// ① 验证码/WAF 优先 —— 这些页面里也夹带「签到」字样\n// ② 再有「已验证签到」的正信号(Discuz 任务:「签到完毕 / 您已完成过此任务 / 恭喜…获得 N 吾爱币」)\n// ③ 才看「去签到链接」还在不在:\n// 论坛(尤其吾爱破解)把当天状态画在图片里(qds.png),文本节点是空的,\n// 但**链接还在 = 服务端认为今天还没签**;签完之后链接就没了。\n// 注意:必须**限定在链接自己所在的那一行**找「任务/签到」字样 —— 任务列表页里\n// 还有其他任务(验证邮箱等)也带 apply 链接,拿整页搜会搞错。\nasync function inspectPage(tabId) {\n const results = await chrome.scripting.executeScript({\n target: { tabId },\n func: () => {\n const title = document.title || '';\n const bodyText = (document.body && document.body.innerText) || '';\n const html = document.documentElement ? document.documentElement.innerHTML : '';\n const all = bodyText + '\\n' + html;\n const compact = bodyText.replace(/\\s+/g, ' ').trim();\n\n // 1) 人机验证 / 验证码 / WAF:优先判定——这些页面里往往也夹带「签到」字样\n const CAPTCHA = ['验证码', '人机验证', '安全验证', '请完成验证', '滑动验证', '滑块', '智能验证', 'slidercaptcha', 'geetest', '极验', 'recaptcha', 'hcaptcha', 'turnstile', 'cf_chl', 'challenge-platform', 'Just a moment', 'checking your browser', 'turing.captcha.qcloud.com', 'TCaptcha', 'CaptchaAId', 'ibex'];\n if (CAPTCHA.some((m) => all.includes(m))) {\n return { ok: false, message: '遇到人机验证/安全验证(验证码/滑块),需人工验证后重试|页面:' + title };\n }\n const WAF = ['waf_zw_verify', 'WZWS_CONFIRM_PREFIX_LABEL', 'Access Denied', '403 Forbidden', '请求被拦截'];\n if (WAF.some((m) => all.includes(m))) {\n return { ok: false, message: '遇到网站安全防护(WAF),请在浏览器完成验证后重试|页面:' + title };\n }\n\n // ---- 找「去签到」入口(Discuz 系论坛通用)----\n // 已签到的视觉特征:文字或图片里直接写着已签/已打卡/连续签到\n const ALREADY = /已签到|已经签到|已打卡|签到完毕|签到完成|连续签到|今日已签|明天再来|下期再来/;\n // 未签到的视觉特征:文字\n const SIGN_TEXT = /打卡签到|立即签到|点击签到|点击打卡|签到领奖|每日签到|每日打卡|签个到|去签到|未打卡|没有签到|还没有签到|今天还没有签到|今天还没有|立即打卡/;\n // 未签到的视觉特征:图片(各 Discuz 签到插件用的图)\n const SIGN_IMG = /qds\\.png|signin_no\\.png|pperwb\\.gif|wb\\.png|qiandao|qdbg|sign_?in/i;\n // 已知的签到按钮 id / class(来自各社区签到脚本:discuz 任务、dsu_paulsign、k_misign、zqlj_sign 等)\n const SIGN_SEL = '#kx,#JD_sign,#addsign,#sg_sign,#dcsignin_tips,#tt_sign,.punch_btn,.go-user-qiandao,a.initiate-checkin,#my_amupper,#pper_a,#sign_title,#setsign,.click-qiandao,.zzhuti_qd_1,.user-index-qd,.taskbtn';\n\n const attrOf = (el) => ((el.getAttribute && el.getAttribute('href') || '') + ' ' +\n (el.id || '') + ' ' + (el.className || '') + ' ' + (el.getAttribute && el.getAttribute('title') || '')).replace(/\\s+/g, ' ');\n const textOf = (el) => String(el.innerText || el.textContent || '').replace(/\\s+/g, '');\n const imgsOf = (el) => Array.from(el.querySelectorAll ? el.querySelectorAll('img') : [])\n .concat(el.tagName === 'IMG' ? [el] : [])\n .map((i) => (i.getAttribute('src') || '') + ' ' + (i.getAttribute('alt') || '')).join(' ');\n\n const findSignLink = () => {\n // ① Discuz 任务申请链接(吾爱破解这类:mod=task&do=apply&id=N)\n const applies = Array.from(document.querySelectorAll('a[href*=\"do=apply\"], a[href*=\"do=draw\"]'))\n .filter((a) => /mod=task/i.test(a.getAttribute('href') || ''));\n for (const a of applies) {\n const own = textOf(a) + ' ' + attrOf(a) + ' ' + imgsOf(a);\n if (ALREADY.test(textOf(a))) continue;\n const row = a.closest('tr') || a.closest('li') || a.closest('table') || a.parentElement;\n const rowTxt = ((row && row.innerText) || '').replace(/\\s+/g, ' ').slice(0, 200);\n // 任务列表里那个「已完成」是隔壁任务的,必须限定在链接所在行\n const rowDone = /已完成|已申请|已领取|已打卡/.test(rowTxt) && !/立即申请|马上申请|申请任务/.test(rowTxt);\n if (rowDone) return { href: a.getAttribute('href') || '', label: (textOf(a) || '签到'), done: true };\n if (/qds\\.png|打卡|每日|签到/i.test(own + ' ' + rowTxt) || applies.length === 1) {\n return { href: a.getAttribute('href') || '', label: (textOf(a) || '打卡签到'), done: false };\n }\n }\n // ② 已知的签到按钮(可能是 <a href>,也可能是 JS 点击的 <span>/<div>/<img>)\n const known = Array.from(document.querySelectorAll(SIGN_SEL));\n for (const el of known) {\n const own = textOf(el) + ' ' + attrOf(el) + ' ' + imgsOf(el);\n if (ALREADY.test(own)) continue;\n const href = (el.getAttribute && el.getAttribute('href')) || '';\n const usable = href && !/^javascript:void/i.test(href) ? href : '';\n return { href: usable, clickSel: true, label: (textOf(el) || '签到'), done: false };\n }\n // ③ 兜底:页面上写着「打卡签到 / 每日签到」的可点元素\n const cands = Array.from(document.querySelectorAll('a,button,[onclick],img'));\n for (const el of cands) {\n const t = textOf(el);\n const im = imgsOf(el);\n if (ALREADY.test(t + ' ' + im)) continue;\n const hit = SIGN_TEXT.test(t) || SIGN_IMG.test(im);\n if (!hit) continue;\n const href = (el.getAttribute && el.getAttribute('href')) || '';\n if (!href && !el.onclick && !el.getAttribute('onclick')) continue; // 纯展示元素,点了也没用\n return { href: /^javascript:/i.test(href) ? '' : href, clickSel: true, label: (t || '签到'), done: false };\n }\n return null;\n };\n const markForClick = (res) => {\n if (!res || !res.clickSel) return '';\n const el = document.querySelector(SIGN_SEL) || (SIGN_TEXT.test(compact) ? document.querySelector('a[href*=\"do=apply\"]') : null);\n if (!el) return '';\n el.setAttribute('data-panel-sign', '1');\n return '[data-panel-sign=\"1\"]';\n };\n\n // 2) 「已完成」的强信号(Discuz:您已完成过此任务 / 今日已签到 / 恭喜…获得…)\n const SIGNED = /签到完毕|签到完成|您已完成过此任务|您已经完成此任务|您今日已经签到|您今天已经签到|今天已经签到|已经签到过|已经完成签到|今日任务已完成|今日已签到|今天已完成签到|已连续签到|下期再来|明天再来|无需重复|恭喜.{0,14}(完成|获得|领到|签到)/;\n // 成功信号:任务页直接告诉用户拿到了什么\n const SUCCESS = /任务已完成|签到成功|打卡成功|领取成功|成功领取|获得.{0,10}(鸡腿|积分|金币|铜币|银币|吾爱币|AK|币|空间|MB|GB)/;\n // 注入环境可能没有 DOM API(部分页面/测试台架):退回到纯文本 + 正则,\n // 只认「do=apply 链接 + 签到图/签到字样」这个最硬的组合。\n let link = null;\n try { link = findSignLink(); } catch (e) { link = null; }\n if (!link) {\n const am = all.match(/<a[^>]*href=[\"']([^\"']*mod=task[^\"']*do=(?:apply|draw)[^\"']*)[\"'][^>]*>/i);\n if (am && /qds\\.png|打卡签到|每日签到|每日打卡|签到完毕/i.test(all) && !/已完成过此任务|您已经完成此任务/.test(all)) {\n link = { href: am[1].replace(/&amp;/g, '&'), label: '打卡签到', done: false };\n }\n }\n if (SUCCESS.test(all)) {\n return { ok: true, message: '签到成功|' + (compact.slice(0, 120) || title) };\n }\n // 强信号优先于「还挂着链接」:例如任务列表里签到那条已「已完成」,\n // 而顶栏还挂着别的任务的 apply 链接\n if (SIGNED.test(all)) return { ok: true, message: '今日已签到,无需重复' };\n if (link && link.done) return { ok: true, message: '今日已签到(任务页显示:已完成)' };\n // 4) 未登录(Discuz 游客页会写「注册[Register]」)—— 比「未签到」更优先:\n // 登录失效时页面导航里也有「每日签到」这类字样,不能把它当成“还没签到”。\n if (/需要先登录|请先登录|还未登录|请登录后|登录已失效|登录失效|请重新登录|未登录|注册\\[Register\\]|立即注册|登录后才能/.test(all)) {\n return { ok: false, message: '登录已失效,请重新获取 Cookie' };\n }\n // 页面上写着「还没签到」却找不到可点的入口(插件式签到页、被遮挡的按钮):\n // 如实报未签到,并说清要人工处理,不要吐一句“未识别到成功标识”。\n if (!link && /打卡签到|立即签到|点击打卡|每日签到|每日打卡|还没有签到|未打卡/.test(all)) {\n return { ok: false, unsigned: true, href: '', clickSel: '',\n message: '网站显示还没签到,但页面上找不到可点的签到入口' };\n }\n if (link) {\n let clickSel = '';\n try { clickSel = markForClick(link); } catch (e) { clickSel = ''; }\n return {\n ok: false, unsigned: true, href: link.href || '', clickSel,\n message: '网站还挂着「' + (link.label || '签到') + '」入口 —— 今天还没签上',\n };\n }\n return { ok: false, message: '未识别到成功标识|页面:' + title + '|' + compact.slice(0, 140) };\n },\n });\n return (results && results[0] && results[0].result) || null;\n}\n\n// 把页面里找到的「去签到」链接解析成绝对地址,并做同源校验。\n// 页面内容不可信:只允许跳到当前站点的同源地址(http/https + 同 host)。\nfunction resolveSignHref(result, tab) {\n const raw = result && result.href;\n if (!raw) return '';\n try {\n const base = (tab && tab.url) || ('https://' + (tab && tab.pendingUrl ? '' : ''));\n const u = new URL(raw, base || undefined);\n const cur = tab && tab.url ? new URL(tab.url) : null;\n if (cur && u.host !== cur.host) return '';\n if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';\n return u.href;\n } catch {\n return '';\n }\n}\n\n// 等待标签页加载完成(带超时),用于导航后复查\nfunction waitTabComplete(tabId, timeoutMs) {\n return new Promise((resolve) => {\n const listener = (id, info) => {\n if (id === tabId && info.status === 'complete') {\n chrome.tabs.onUpdated.removeListener(listener);\n resolve();\n }\n };\n chrome.tabs.onUpdated.addListener(listener);\n setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, timeoutMs || 20000);\n });\n}\n\n// 尝试执行面板下发的脚本字符串。\n// 注意:MV3 禁止 new Function / eval,多数情况下这里会失败——失败时由调用方回退到原生巡检。\nasync function runPanelScript(tabId, job) {\n const execPromise = chrome.scripting.executeScript({\n target: { tabId },\n func: (scriptStr, params) => {\n const fn = new Function('params', `return (${scriptStr})(params)`);\n return fn(params);\n },\n args: [job.script, job.params || {}],\n });\n const timeoutPromise = new Promise((_, reject) =>\n setTimeout(() => reject(new Error('脚本执行超时(60秒)')), 60000)\n );\n const results = await Promise.race([execPromise, timeoutPromise]);\n return (results && results[0] && results[0].result) || null;\n}\n\n// 在目标域名的标签页中执行签到脚本\nasync function executeJob(job) {\n const startMs = Date.now();\n let tab = null;\n let created = false;\n try {\n // 与 popup 保持一致:缺域名时直接报错,避免误开 https://undefined/ 标签页\n if (!job.domain) throw new Error('任务缺少目标域名');\n // 找一个该域名的已存在标签页,复用;没有则新建隐藏标签页\n const tabs = await chrome.tabs.query({ url: `*://${job.domain}/*` });\n if (tabs.length > 0) {\n tab = tabs[0];\n } else {\n tab = await chrome.tabs.create({ url: `https://${job.domain}/`, active: false });\n created = true;\n // 等待页面加载完成\n await new Promise((resolve) => {\n const listener = (tabId, info) => {\n if (tabId === tab.id && info.status === 'complete') {\n chrome.tabs.onUpdated.removeListener(listener);\n resolve();\n }\n };\n chrome.tabs.onUpdated.addListener(listener);\n setTimeout(resolve, 15000); // 超时保护\n });\n }\n\n // 如果任务指定了导航 URL(如吾爱破解),先导航到该页面(模拟手动点击),再执行检查脚本\n if (job.navigate_url) {\n await chrome.tabs.update(tab.id, { url: job.navigate_url });\n await new Promise((resolve) => {\n const listener = (tabId, info) => {\n if (tabId === tab.id && info.status === 'complete') {\n chrome.tabs.onUpdated.removeListener(listener);\n resolve();\n }\n };\n chrome.tabs.onUpdated.addListener(listener);\n setTimeout(resolve, 20000);\n });\n await new Promise((r) => setTimeout(r, 2000));\n }\n\n // 执行签到。\n // MV3 禁止 new Function / eval,面板下发的脚本字符串通常无法执行;\n // 因此:有 navigate_url 的任务直接对目标页做原生巡检;否则先试面板脚本,拿不到结果就回退原生巡检。\n let result = null;\n if (!job.navigate_url && job.script) {\n try {\n result = await runPanelScript(tab.id, job);\n } catch (e) {\n console.log('[签到面板] 面板脚本执行失败,回退原生巡检:', e.message || e);\n result = null;\n }\n }\n if (!result) result = await inspectPage(tab.id);\n if (!result) throw new Error('无法读取页面内容(标签页可能已关闭或被重定向)');\n\n // 页面还挂着「打卡签到」入口 = 服务端认为今天没签。\n // ① 是链接(吾爱破解这类 Discuz 任务)→ 导航过去,等同人手点一下;\n // ② 是 JS 按钮(dsu_paulsign / k_misign 等插件)→ 在页面里点一下。\n // 两条路都是真实用户行为,之前实测过:脚本化 fetch 会被 WAF 吊死,而导航/点击能过。\n if (!result.ok && result.unsigned) {\n const signHref = resolveSignHref(result, tab);\n try {\n if (signHref) {\n await chrome.tabs.update(tab.id, { url: signHref });\n await waitTabComplete(tab.id, 20000);\n await new Promise((r) => setTimeout(r, 2000));\n } else if (result.clickSel) {\n await chrome.scripting.executeScript({\n target: { tabId: tab.id },\n func: (sel) => { const el = document.querySelector(sel); if (el) el.click(); },\n args: [result.clickSel],\n });\n await new Promise((r) => setTimeout(r, 3000));\n }\n const after = (signHref || result.clickSel) ? await inspectPage(tab.id) : null;\n if (after) result = after;\n } catch (e) {\n console.log('[签到面板] 触发签到失败:', e.message || e);\n }\n }\n\n // 连入口都没认出来(站点改版/插件页),但面板给了「签到地址」——那是站点模块自己知道的,\n // 再试一次,别白自放弃(同源校验后仍然只跳本域)。\n if (!result.ok && result.unsigned && !result.href && !result.clickSel && job.sign_url) {\n try {\n const u = new URL(job.sign_url);\n const sameHost = !job.domain || u.hostname === job.domain || u.hostname.endsWith('.' + job.domain);\n if ((u.protocol === 'http:' || u.protocol === 'https:') && sameHost) {\n await chrome.tabs.update(tab.id, { url: u.href });\n await waitTabComplete(tab.id, 20000);\n await new Promise((r) => setTimeout(r, 2000));\n const after = await inspectPage(tab.id);\n if (after) result = after;\n }\n } catch (e) {\n console.log('[签到面板] 兑底签到地址不可用:', e.message || e);\n }\n }\n\n // 明确未签到、且始终找不到可点的入口/地址 → 把「接下来怎么办」说清楚,\n // 不要只丢一句含糊的“未识别到成功标识”。\n if (!result.ok && result.unsigned && !result.href && !result.clickSel) {\n result = { ...result, message: String(result.message || '今天还没签上') + ';页面上找不到可点的签到入口,请在浏览器手动签一次' };\n }\n\n return {\n status: result.ok ? 'ok' : 'fail',\n message: String(result.message || (result.ok ? '签到成功' : '签到失败')),\n durationMs: Date.now() - startMs,\n };\n } catch (e) {\n return {\n status: 'fail',\n message: '浏览器执行失败:' + (e.message || e),\n durationMs: Date.now() - startMs,\n };\n } finally {\n // 只关闭“我们自己新建”的标签页(复用的、用户正在看的保留),避免每次签到累积一堆隐藏标签页\n if (created && tab && tab.id) {\n chrome.tabs.remove(tab.id).catch(() => {});\n }\n }\n}\n\n// 主流程:获取任务 → 逐个执行 → 上报\nasync function runJobs() {\n const { panelUrl, apiKey } = await getConfig();\n if (!panelUrl || !apiKey) {\n console.log('[签到面板] 未配置面板地址或 API Key,跳过');\n return;\n }\n\n let jobs;\n try {\n jobs = await fetchJobs(panelUrl, apiKey);\n } catch (e) {\n console.log('[签到面板] 获取任务失败:', e.message);\n return;\n }\n\n if (!jobs.length) {\n console.log('[签到面板] 暂无待执行任务');\n return;\n }\n\n console.log(`[签到面板] 获取到 ${jobs.length} 个任务,开始执行`);\n for (const job of jobs) {\n console.log(`[签到面板] 执行:${job.site_name}(账号 ${job.account_id})`);\n const r = await executeJob(job);\n console.log(`[签到面板] 结果:${r.status} - ${r.message}`);\n await reportResult(panelUrl, apiKey, job.account_id, r.status, r.message, r.durationMs);\n // 任务之间稍作间隔\n await new Promise((r) => setTimeout(r, 2000));\n }\n}\n\n// ============ 本地网络中继代理 ============\n// Worker 把 HTTP 请求存入面板队列,扩展用用户本地网络执行后回传响应。\n// 这样 Worker 端的站点逻辑可以使用用户本地 IP,绕过 CF/Worker IP 限制。\n\n// base64 ↔ Uint8Array\nfunction b64ToBytes(b64) {\n const s = atob(b64);\n const arr = new Uint8Array(s.length);\n for (let i = 0; i < s.length; i++) arr[i] = s.charCodeAt(i);\n return arr;\n}\nfunction bytesToB64(bytes) {\n let s = '';\n for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);\n return btoa(s);\n}\n\n// 获取待执行的中继任务(长轮询:没有任务时挂起,一有任务立刻返回)\nasync function fetchRelayJobs(panelUrl, apiKey, waitMs = RELAY_LONGPOLL_MS) {\n // 带上扩展版本(?v=),面板可以显示「扩展在线 · v2.2」并提醒版本陈旧\n let ver = '';\n try { ver = (chrome.runtime.getManifest() || {}).version || ''; } catch { /* 忽略 */ }\n const resp = await fetch(panelUrl + '/api/external/relay-pending?wait=' + waitMs + (ver ? '&v=' + encodeURIComponent(ver) : ''), {\n headers: { 'X-Api-Key': apiKey },\n });\n if (!resp.ok) throw new Error('HTTP ' + resp.status);\n const data = await resp.json();\n return data.jobs || [];\n}\n\n// 回传中继结果\nasync function submitRelayResult(panelUrl, apiKey, jobId, result) {\n await fetch(panelUrl + '/api/external/relay/' + jobId + '/result', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Api-Key': apiKey },\n body: JSON.stringify(result),\n }).catch(() => { /* 忽略 */ });\n}\n\n// 在目标域名的页面上下文中执行单个 HTTP 请求(携带用户 Cookie)\n//\n// 为什么要「先短超时、超时就激活标签重试」:\n// 实测(2026-09-28)发现,对 www.52pojie.cn 这类**带反爬挑战的主站**,从**后台标签**发\n// fetch 会一直挂到硬超时(同一时刻同一浏览器:example.com 与 static.52pojie.cn 都正常返回 200,\n// 只有主站的 robots.txt / portal.php / home.php 全部超时)。\n// 原因是挑战/验证脚本在后台标签里被节流,服务端一直等不到「验证完成」,连接就不回包。\n// 所以:第一次用短超时快速试探;若超时,就把标签**激活到前台**再试一次,跑完把焦点还给用户。\nasync function executeRelayJob(job) {\n const url = new URL(job.url);\n const domain = url.hostname;\n let tab = null;\n let created = false;\n let prevActiveId = null;\n let activated = false;\n try {\n const tabs = await chrome.tabs.query({ url: `*://${domain}/*` });\n if (tabs.length > 0) {\n tab = tabs[0];\n } else {\n tab = await chrome.tabs.create({ url: `https://${domain}/`, active: false });\n created = true;\n await new Promise((resolve) => {\n const listener = (tabId, info) => {\n if (tabId === tab.id && info.status === 'complete') {\n chrome.tabs.onUpdated.removeListener(listener);\n resolve();\n }\n };\n chrome.tabs.onUpdated.addListener(listener);\n setTimeout(resolve, 8000); // 建标签页只为拿登录态,页面没加载完也照样能发请求,不必等满\n });\n }\n\n const injectFetch = async (url, method, headers, bodyB64, options, timeoutMs) => {\n const b64ToBytes = (b64) => {\n const s = atob(b64);\n const arr = new Uint8Array(s.length);\n for (let i = 0; i < s.length; i++) arr[i] = s.charCodeAt(i);\n return arr;\n };\n const bytesToB64 = (bytes) => {\n let s = '';\n for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);\n return btoa(s);\n };\n const init = {\n method,\n headers,\n credentials: 'include', // 始终携带用户 Cookie\n // 页面里拿不到 opaqueredirect 的响应头,manual 等于白跑一轮,统一 follow\n redirect: options.redirect === 'manual' ? 'follow' : (options.redirect || 'follow'),\n };\n if (bodyB64) init.body = b64ToBytes(bodyB64);\n const ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;\n if (ctrl) init.signal = ctrl.signal;\n let timer = null;\n // 不依赖 AbortController:某些受限环境里它不存在,那样整个任务会挂到硬超时。\n // 用 Promise.race 做兜底,保证无论如何都在 timeoutMs 内**返回**(返回超时标记,不抛出)。\n const timeout = new Promise((resolve) => {\n timer = setTimeout(() => {\n try { if (ctrl) ctrl.abort(); } catch { /* 忽略 */ }\n resolve({ timeout: true });\n }, timeoutMs || 18000);\n });\n const call = (async () => {\n const resp = await fetch(url, init);\n const buf = new Uint8Array(await resp.arrayBuffer());\n const h = {};\n resp.headers.forEach((v, k) => { h[k] = v; });\n return {\n status: resp.status,\n headers: h,\n body_base64: bytesToB64(buf),\n url: resp.url, // 最终 URL(跟随重定向后)\n };\n })();\n try {\n return await Promise.race([call, timeout]);\n } finally {\n clearTimeout(timer);\n call.catch(() => {}); // 超时后迟到的失败不要变成未处理拒绝\n }\n };\n\n const runOnce = async (timeoutMs) => {\n const results = await chrome.scripting.executeScript({\n target: { tabId: tab.id },\n func: injectFetch,\n args: [job.url, job.method, job.headers || {}, job.body_base64 || null, job.options || {}, timeoutMs],\n });\n return (results && results[0] && results[0].result) || null;\n };\n\n let r = await runOnce(RELAY_FIRST_TRY_MS);\n if (r && r.timeout) {\n // 后台标签里反爬挑战跑不动 → 把标签拿到前台重试一次\n try {\n const [cur] = await chrome.tabs.query({ active: true, currentWindow: true });\n prevActiveId = cur && cur.id != null ? cur.id : null;\n await chrome.tabs.update(tab.id, { active: true });\n activated = true;\n await new Promise((res) => setTimeout(res, RELAY_ACTIVATE_WAIT_MS)); // 给页面里的定时/挑战脚本一点时间\n } catch { /* 激活失败也得继续试 */ }\n r = await runOnce(RELAY_FETCH_TIMEOUT_MS);\n }\n if (!r) throw new Error('无返回结果');\n if (r.timeout) {\n // 把话说清楚:请求**已经发出去了**,只是没等到响应。\n // 签到这类写操作可能已经生效,面板不能当成「失败」(面板会记为「结果未知」并稍后自动复核)。\n return {\n error: activated\n ? `中继请求超时(前台标签也没等到响应,${Math.round(RELAY_FETCH_TIMEOUT_MS / 1000)}秒):请求已发出但没收到回包。该站点的反爬可能要求人工验证,或站点暂时不可达 —— 若站点其实处理了,签到可能已生效`\n : `中继请求超时(${Math.round(RELAY_FETCH_TIMEOUT_MS / 1000)}秒):请求已发出但没收到回包,站点可能已记录(结果未知)`,\n };\n }\n // 把最终 URL 放入特殊头,Worker 端可读取\n const headers = { ...(r.headers || {}), 'x-relay-url': r.url || job.url };\n return { status: r.status, headers, body_base64: r.body_base64 };\n } catch (e) {\n return { error: String(e.message || e).slice(0, 500) };\n } finally {\n // 把焦点还给用户原来的标签\n if (activated && prevActiveId != null) {\n chrome.tabs.update(prevActiveId, { active: true }).catch(() => {});\n }\n // 复用的标签页保留,自己新建的关闭(避免堆积)\n if (created && tab && tab.id) {\n chrome.tabs.remove(tab.id).catch(() => {});\n }\n }\n}\n\n// 中继主循环(单轮):取任务 → 执行 → 回传\n// 若正好有一轮在跑,先等它(避免「刚排队的任务被这轮跳过」),最多等 ~600ms。\nasync function runRelayOnce() {\n for (let i = 0; i < 3 && relayBusy; i++) await sleep(200);\n if (relayBusy) return { ok: true, count: 0, skipped: true };\n relayBusy = true;\n try {\n return await runRelayRound();\n } finally {\n relayBusy = false;\n }\n}\n\nasync function runRelayRound() {\n const { panelUrl, apiKey } = await getConfig();\n if (!panelUrl || !apiKey) return { ok: false, reason: 'noconfig' };\n let jobs;\n try {\n jobs = await fetchRelayJobs(panelUrl, apiKey);\n } catch (e) {\n return { ok: false, error: String((e && e.message) || e) }; // 面板不可达时静默重试\n }\n for (const job of jobs) {\n try {\n console.log(`[签到面板] 中继请求:${job.method} ${job.url}`);\n // 硬超时:即使页面 fetch 或注入挂起,也必须回传结果,否则任务会永久卡在队列\n const result = await Promise.race([\n executeRelayJob(job),\n new Promise((resolve) => setTimeout(() => resolve({ error: `中继执行超时(${Math.round(RELAY_JOB_TIMEOUT_MS / 1000)}秒):请求已发出但没收到回包,站点可能已记录(结果未知)` }), RELAY_JOB_TIMEOUT_MS)),\n ]);\n await submitRelayResult(panelUrl, apiKey, job.id, result);\n } catch (e) {\n console.error(`[签到面板] 中继任务 ${job.id} 失败:`, e);\n // 回传错误,避免任务卡在 pending\n try {\n await submitRelayResult(panelUrl, apiKey, job.id, { error: String(e.message || e) });\n } catch { /* 忽略 */ }\n }\n }\n return { ok: true, count: jobs.length };\n}\n\n// 中继长轮询「突发」:一轮结束立刻接下一轮,让面板点「执行」后 1 秒内就被领走。\n//\n// 为什么不用 while(true) 常驻循环:① 浏览器可能随时回收 Service Worker,常驻循环早晚会断;\n// ② 若面板侧不支持长轮询(旧版本),常驻循环会变成毫秒级空转打网络。\n// 所以改成「有界突发」:最多 RELAY_BURST_ROUNDS 轮,连续 RELAY_BURST_IDLE 轮没任务就结束;\n// 由 RELAY_ALARM(0.5 分钟)和 popup 的「立即执行」反复拉起,接得上就与常驻无异。\nlet relayGen = 0; // 代际令牌:新的突发会作废旧的,避免多份循环并发抢任务\nlet relayBusy = false; // 正在处理一轮长轮询(避免并发取同一批任务)\n\nfunction sleep(ms) {\n return new Promise((res) => setTimeout(res, ms));\n}\n\nfunction startRelayBurst(opts = {}) {\n const rounds = opts.rounds || RELAY_BURST_ROUNDS;\n const idleStop = opts.idleStop || RELAY_BURST_IDLE;\n const gen = ++relayGen;\n let idle = 0;\n (async () => {\n for (let i = 0; i < rounds; i++) {\n if (gen !== relayGen) return; // 已被更新的突发取代/已停止\n let r;\n try {\n r = await runRelayOnce();\n } catch (e) {\n r = { ok: false, error: String((e && e.message) || e) };\n }\n if (gen !== relayGen) return;\n if (r && r.reason === 'noconfig') return; // 未配置面板地址/Key,不空转\n if (r && r.count) idle = 0; else idle++;\n if (idle >= idleStop) return; // 一连几轮都没任务,先歇着,交给下次 alarm\n await sleep(r && r.ok ? RELAY_GAP_MS : 5000);\n }\n })();\n}\n\n// 供 popup / 测试使用:立刻停掉当前突发(不打断正在执行的那一轮)\nfunction stopRelayBurst() {\n relayGen++;\n}\n\n// 定时器\nchrome.alarms.onAlarm.addListener((alarm) => {\n if (alarm.name === ALARM_NAME) runJobs();\n if (alarm.name === RELAY_ALARM) startRelayBurst();\n});\n\n// 扩展安装/启动时设置定时器\nchrome.runtime.onInstalled.addListener(() => {\n chrome.alarms.create(ALARM_NAME, { periodInMinutes: CHECK_INTERVAL_MIN });\n chrome.alarms.create(RELAY_ALARM, { periodInMinutes: RELAY_INTERVAL_MIN });\n // 安装后立即跑一次(方便验证),并起一次中继突发\n setTimeout(runJobs, 5000);\n startRelayBurst();\n});\n\nchrome.runtime.onStartup.addListener(() => {\n chrome.alarms.create(ALARM_NAME, { periodInMinutes: CHECK_INTERVAL_MIN });\n chrome.alarms.create(RELAY_ALARM, { periodInMinutes: RELAY_INTERVAL_MIN });\n startRelayBurst();\n});\n\n// Service Worker 每次启动时确保定时器存在(覆盖\"重新加载\"场景:onInstalled/onStartup 都不触发)\nchrome.alarms.get(ALARM_NAME, (a) => {\n if (!a) chrome.alarms.create(ALARM_NAME, { periodInMinutes: CHECK_INTERVAL_MIN });\n});\nchrome.alarms.get(RELAY_ALARM, (a) => {\n if (!a) chrome.alarms.create(RELAY_ALARM, { periodInMinutes: RELAY_INTERVAL_MIN });\n});\n\n// popup 手动触发\nchrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {\n if (msg && msg.action === 'runJobsNow') {\n runJobs().then(() => sendResponse({ ok: true })).catch((e) => sendResponse({ ok: false, error: String(e) }));\n return true; // 异步响应\n }\n if (msg && msg.action === 'runRelayNow') {\n // 先同步跑完一轮(已排队的任务立即执行),响应后再转入长轮询突发\n runRelayOnce()\n .then((r) => {\n sendResponse({ ok: true, count: (r && r.count) || 0 });\n startRelayBurst();\n })\n .catch((e) => sendResponse({ ok: false, error: String((e && e.message) || e) }));\n return true;\n }\n if (msg && msg.action === 'stopRelay') {\n stopRelayBurst();\n sendResponse({ ok: true });\n return true;\n }\n});\n",
"@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": "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",
"@b64:icon128.png": "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",
};