first commit

This commit is contained in:
2026-06-15 10:06:03 +08:00
commit ec826c55ff
8 changed files with 667 additions and 0 deletions
+317
View File
@@ -0,0 +1,317 @@
// popup.js — 读 xueqiu.com 的 HttpOnly Cookie,弹窗展示 + 一键复制。
//
// 浏览器扩展能直接读 HttpOnly cookiechrome.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 = [];
// 模式 Adomain 模式
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:分桶 cookieChrome 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();