// popup.js — 读 xueqiu.com 的 HttpOnly Cookie,弹窗展示 + 一键复制。 // // 浏览器扩展能直接读 HttpOnly cookie(chrome.cookies API), // 但不能直连 PG/MySQL(沙箱限制)。所以这里只读 + 展示 + 复制。 // 复制完手动粘到 .env / dashboard / 任何需要的地方。 // // v0.2 改动: // 1) 尝试多种 chrome.cookies 查询模式(domain / url / 带 partitionKey) // 2) 同时从 content script 拉页面 document.cookie // 3) 手动粘贴兜底(万一所有自动读都失败) const $xq = document.getElementById("xq-val"); const $u = document.getElementById("u-val"); const $combined = document.getElementById("combined-val"); const $msg = document.getElementById("msg"); const $debug = document.getElementById("debug"); const $manual = document.getElementById("manual-textarea"); const $useManual = document.getElementById("use-manual"); let COOKIES = { xq_a_token: "", u: "" }; let ALL_COOKIES = []; let PAGE_COOKIES = {}; function setMsg(text, level = "info") { $msg.className = level; $msg.textContent = text; } function clearMsg() { $msg.className = ""; $msg.textContent = ""; } function setVal(el, v, isEmpty) { el.textContent = v || "未读取"; el.classList.toggle("empty", !!isEmpty); } function combinedString() { const { xq_a_token, u } = COOKIES; if (!xq_a_token || !u) return ""; return `xq_a_token=${xq_a_token};u=${u}`; } function mergeCookies(...lists) { const seen = new Set(); const out = []; for (const list of lists) { for (const c of list || []) { const key = `${c.domain}|${c.path}|${c.name}`; if (!seen.has(key)) { seen.add(key); out.push(c); } } } return out; } function tryParseCookieString(s) { // 接受 "a=1; b=2" 格式 const out = {}; if (!s) return out; const norm = s.replace(/\n/g, ";").replace(/\r/g, ";"); for (const part of norm.split(";")) { const idx = part.indexOf("="); if (idx > 0) { const k = part.slice(0, idx).trim(); const v = part.slice(idx + 1).trim(); if (k) out[k] = v; } } return out; } async function readCookies() { clearMsg(); ALL_COOKIES = []; PAGE_COOKIES = {}; if (typeof chrome === "undefined" || !chrome.cookies) { setMsg("❌ chrome.cookies API 不可用 — 扩展可能没正确加载", "err"); return; } const errors = []; const queries = []; // 模式 A:domain 模式 try { const a = await chrome.cookies.getAll({ domain: ".xueqiu.com" }); queries.push({ mode: "domain=.xueqiu.com", n: a.length, list: a }); } catch (e) { errors.push(`domain=.xueqiu.com: ${e.message || e}`); } try { const a = await chrome.cookies.getAll({ domain: "xueqiu.com" }); queries.push({ mode: "domain=xueqiu.com (exact)", n: a.length, list: a }); } catch (e) { errors.push(`domain=xueqiu.com: ${e.message || e}`); } // 模式 B:url 模式(多个变体) const urls = [ "https://xueqiu.com/", "https://www.xueqiu.com/", "https://xueqiu.com", "https://www.xueqiu.com", ]; for (const u of urls) { try { const r = await chrome.cookies.getAll({ url: u }); queries.push({ mode: `url=${u}`, n: r.length, list: r }); } catch (e) { errors.push(`url=${u}: ${e.message || e}`); } } // 模式 C:按 cookie 名精确查(不走 list,直接 single get) for (const u of urls) { for (const name of ["xq_a_token", "u"]) { try { const c = await chrome.cookies.get({ url: u, name }); if (c) queries.push({ mode: `get ${name} @ ${u}`, n: 1, list: [c] }); } catch (e) { // 静默:找不到是正常的 } } } // 模式 D:分桶 cookie(Chrome 115+ 默认 partition 行为) for (const u of urls) { try { const r = await chrome.cookies.getAll({ url: u, partitionKey: { topLevelSite: "https://xueqiu.com" }, }); queries.push({ mode: `url=${u} (partitioned TLS=xueqiu.com)`, n: r.length, list: r }); } catch (e) { // 忽略 } } // 模式 E:从 content script 读(page document.cookie) try { const tabs = await chrome.tabs.query({ url: ["https://xueqiu.com/*", "https://*.xueqiu.com/*"], }); for (const t of tabs) { try { const resp = await chrome.tabs.sendMessage(t.id, { type: "xueqiu_sync/get_page_cookies", }); if (resp && resp.cookies) { PAGE_COOKIES = { ...PAGE_COOKIES, ...resp.cookies }; queries.push({ mode: `page @ ${t.url}`, n: Object.keys(resp.cookies).length, list: [] }); // 把 PAGE_COOKIES 转成伪 cookie 对象参与合并 for (const [k, v] of Object.entries(resp.cookies)) { queries[queries.length - 1].list.push({ domain: "(page)", path: "/", name: k, value: v, }); } } } catch (_) { // content script 没注入 } } } catch (e) { errors.push(`tabs query: ${e.message || e}`); } // 合并所有模式的 cookie const allLists = queries.map((q) => q.list); ALL_COOKIES = mergeCookies(...allLists); console.log("[xueqiu_sync] queries:", queries); console.log("[xueqiu_sync] merged:", ALL_COOKIES); const xq = ALL_COOKIES.find((c) => c.name === "xq_a_token" && c.value); const u = ALL_COOKIES.find((c) => c.name === "u" && c.value); COOKIES = { xq_a_token: xq ? xq.value : "", u: u ? u.value : "", }; setVal($xq, COOKIES.xq_a_token, !COOKIES.xq_a_token); setVal($u, COOKIES.u, !COOKIES.u); $combined.value = combinedString() || "未读取"; // 扩展自身信息 let extInfo = null; try { extInfo = { id: chrome.runtime.id, manifest: chrome.runtime.getManifest(), }; } catch (e) { extInfo = { id: "(no chrome.runtime)", err: e.message }; } renderDebug(queries, errors, extInfo); // 状态判断 if (ALL_COOKIES.length === 0) { setMsg( "❌ 一个 xueqiu.com cookie 都没拿到。\n" + "请展开下方「调试信息」看具体哪些查询返回 0 个。" + "如果只有 page @ 拿到、其它都 0:可能是分桶 cookie,需要等 Chrome 改进。\n" + "如果全部都 0:可能是 Chrome profile 不一致 / 扩展权限没生效。", "err", ); } else if (!COOKIES.xq_a_token) { const names = ALL_COOKIES.map((c) => c.name).join(", "); setMsg( `❌ 拿到了 ${ALL_COOKIES.length} 个 cookie 但没有 xq_a_token。\n字段:${names}\n` + "xq_a_token 是 HttpOnly。如果只能从 page 读到 u(无 xq_a_token):\n" + "请展开调试区看「get xq_a_token @ ...」的查询结果。", "err", ); } else if (!COOKIES.u) { setMsg("❌ 缺 u 字段 — Cookie 不完整", "err"); } else { setMsg(`✅ 已读取 xq_a_token + u(共 ${ALL_COOKIES.length} 个 xueqiu.com cookie)`, "ok"); } } function renderDebug(queries, errors, extInfo) { const lines = []; if (extInfo) { lines.push(`[扩展] id=${extInfo.id}`); if (extInfo.manifest) { const perms = (extInfo.manifest.permissions || []).join(", "); const hosts = (extInfo.manifest.host_permissions || []).join(", "); lines.push(`[扩展] permissions=${perms}`); lines.push(`[扩展] host_permissions=${hosts}`); lines.push(`[扩展] manifest_version=${extInfo.manifest.manifest_version}`); } } for (const q of queries) { lines.push(`[查询] ${q.mode} → ${q.n} 个`); } if (errors && errors.length) { lines.push("[错误](仅 show failure)"); for (const e of errors) lines.push(" - " + e); } if (ALL_COOKIES.length) { lines.push("[合并后] 列表:"); for (const c of ALL_COOKIES) { const v = (c.value || "").slice(0, 40); lines.push(` ${c.domain} ${c.path} ${c.name}=${v}${c.value && c.value.length > 40 ? "…" : ""}`); } } $debug.textContent = lines.join("\n"); } async function copyText(text, label) { if (!text) { setMsg("❌ 没有可复制的内容(Cookie 缺失)", "err"); return; } try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); } else { const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); } setMsg(`✅ 已复制:${label}`, "ok"); } catch (e) { setMsg(`❌ 复制失败:${e.message || e}`, "err"); } } document.getElementById("copy-combined").addEventListener("click", () => { copyText(combinedString(), "xq_a_token=...;u=..."); }); document.getElementById("copy-env").addEventListener("click", () => { copyText(`XUEQIU_TOKEN=${combinedString()}`, "XUEQIU_TOKEN=..."); }); document.getElementById("copy-xq").addEventListener("click", () => { copyText(COOKIES.xq_a_token, "xq_a_token (单独值)"); }); document.getElementById("reload-btn").addEventListener("click", () => { readCookies(); }); // 手动粘贴兜底:用户从 DevTools Network tab 复制 Cookie header 粘到这里 $useManual.addEventListener("click", () => { const text = $manual.value.trim(); if (!text) { setMsg("❌ 粘贴框为空", "err"); return; } const pairs = tryParseCookieString(text); const xq = pairs["xq_a_token"] || ""; const u = pairs["u"] || ""; if (!xq || !u) { setMsg(`❌ 粘贴内容里找不到 xq_a_token 和/或 u(找到字段:${Object.keys(pairs).join(", ")})`, "err"); return; } COOKIES = { xq_a_token: xq, u }; setVal($xq, xq, false); setVal($u, u, false); $combined.value = combinedString(); setMsg("✅ 已从手动粘贴提取(仅当前会话有效)", "ok"); }); // 弹窗打开时立刻读一次 readCookies();