first commit
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
# xueqiu_sync — 雪球 Cookie 复制 Chrome 扩展
|
||||||
|
|
||||||
|
把本地 Chrome 里 xueqiu.com 的 HttpOnly Cookie 读出来,一键复制成你需要的格式。
|
||||||
|
**不依赖任何后端 / 数据库 / Python**。
|
||||||
|
|
||||||
|
## 为什么不做成自动化
|
||||||
|
|
||||||
|
浏览器扩展**不能**直接连 PostgreSQL / MySQL(沙箱限制:无 DB 驱动、禁裸 TCP、暴露凭据)。
|
||||||
|
所以这版只做"读 cookie + 复制到剪贴板",你粘到哪里由你决定。
|
||||||
|
|
||||||
|
## 准备
|
||||||
|
|
||||||
|
- Chrome / Edge / 其他 Chromium 内核浏览器
|
||||||
|
- 一次:在浏览器里登录过 [xueqiu.com](https://xueqiu.com/)(让 cookie 落到本地)
|
||||||
|
|
||||||
|
## 安装(开发模式,30 秒)
|
||||||
|
|
||||||
|
1. 打开 `chrome://extensions/`
|
||||||
|
2. 右上角打开「**开发者模式**」
|
||||||
|
3. 点「**加载已解压的扩展程序**」
|
||||||
|
4. 选这个目录的 `chrome-extension/` 子目录:
|
||||||
|
```
|
||||||
|
/home/gao/Development/quant_home/xueqiu_sync/chrome-extension/
|
||||||
|
```
|
||||||
|
5. 工具栏会出现一个 ❄ 雪花图标
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
1. 浏览器登录过 xueqiu.com 后,点工具栏的 ❄ 图标
|
||||||
|
2. 弹窗会**自动**读出 `xq_a_token` 和 `u`(含 HttpOnly)
|
||||||
|
3. 选一个复制按钮:
|
||||||
|
|
||||||
|
| 按钮 | 复制内容 | 用在 |
|
||||||
|
|---|---|---|
|
||||||
|
| **复制组合字符串** | `xq_a_token=xxx;u=yyy` | dashboard 的 `POST /api/data/datasource/xueqiu-cookie`、`market_sync` 的 `XUEQIU_TOKEN` 字段值 |
|
||||||
|
| **复制 .env 行** | `XUEQIU_TOKEN=xq_a_token=xxx;u=yyy` | 直接粘到 `market_sync/.env` |
|
||||||
|
| **仅 xq_a_token** | `xxx` | 单独使用场景 |
|
||||||
|
|
||||||
|
## 改代码
|
||||||
|
|
||||||
|
`chrome://extensions/` → 找到本扩展 → 点 ↻ 重新加载。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
xueqiu_sync/
|
||||||
|
├── README.md
|
||||||
|
├── .gitignore
|
||||||
|
└── chrome-extension/
|
||||||
|
├── manifest.json
|
||||||
|
├── popup.html
|
||||||
|
├── popup.js
|
||||||
|
└── icons/
|
||||||
|
├── icon16.png
|
||||||
|
├── icon48.png
|
||||||
|
└── icon128.png
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
私有
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// content.js — 在 xueqiu.com 页面里跑,document.cookie 读非 HttpOnly 的 cookie,
|
||||||
|
// 然后缓存起来等 popup 来取。
|
||||||
|
// 注意:document.cookie 读不到 HttpOnly(xq_a_token 是 HttpOnly 拿不到)
|
||||||
|
// 但能拿到 u、device_id 等普通 cookie,作为辅助数据。
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const PAGE_CACHE_KEY = "__xueqiu_sync_page_cache__";
|
||||||
|
const TTL_MS = 30_000;
|
||||||
|
|
||||||
|
function read() {
|
||||||
|
const out = { url: location.href, cookies: {}, ts: Date.now() };
|
||||||
|
try {
|
||||||
|
const pairs = document.cookie.split(";");
|
||||||
|
for (const p of pairs) {
|
||||||
|
const idx = p.indexOf("=");
|
||||||
|
if (idx > 0) {
|
||||||
|
const k = p.slice(0, idx).trim();
|
||||||
|
const v = decodeURIComponent(p.slice(idx + 1).trim());
|
||||||
|
if (k) out.cookies[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
try {
|
||||||
|
window[PAGE_CACHE_KEY] = read();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
// 兜底:每 5s 刷一次,捕获导航/SPA 切换
|
||||||
|
setInterval(update, 5000);
|
||||||
|
|
||||||
|
// 响应 popup 的查询
|
||||||
|
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||||
|
if (msg && msg.type === "xueqiu_sync/get_page_cookies") {
|
||||||
|
let cache = window[PAGE_CACHE_KEY];
|
||||||
|
if (!cache || Date.now() - cache.ts > TTL_MS) {
|
||||||
|
cache = read();
|
||||||
|
window[PAGE_CACHE_KEY] = cache;
|
||||||
|
}
|
||||||
|
sendResponse(cache);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 892 B |
Binary file not shown.
|
After Width: | Height: | Size: 146 B |
Binary file not shown.
|
After Width: | Height: | Size: 303 B |
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "雪球 Cookie 复制",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "读取本地 xueqiu.com 的 HttpOnly Cookie,一键复制到剪贴板(多种格式)。",
|
||||||
|
"permissions": ["cookies", "clipboardWrite", "tabs"],
|
||||||
|
"host_permissions": [
|
||||||
|
"https://xueqiu.com/*",
|
||||||
|
"https://*.xueqiu.com/*"
|
||||||
|
],
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["https://xueqiu.com/*", "https://*.xueqiu.com/*"],
|
||||||
|
"js": ["content.js"],
|
||||||
|
"run_at": "document_idle"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_title": "复制雪球 Cookie",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>雪球 Cookie 复制</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font: 13px/1.5 -apple-system, "Segoe UI", "PingFang SC", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 14px;
|
||||||
|
width: 380px;
|
||||||
|
background: #fafbfc;
|
||||||
|
color: #1f2328;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
h1::before {
|
||||||
|
content: "❄";
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.row label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #57606a;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.val {
|
||||||
|
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 5px 7px;
|
||||||
|
background: #f6f8fa;
|
||||||
|
border: 1px solid #d0d7de;
|
||||||
|
border-radius: 5px;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 60px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.val.empty {
|
||||||
|
color: #b91c1c;
|
||||||
|
background: #fef2f2;
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
|
textarea.val {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
.btn-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.btn-row.full {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 7px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
button.secondary {
|
||||||
|
background: #fff;
|
||||||
|
color: #1f2328;
|
||||||
|
border: 1px solid #d0d7de;
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
background: #94a3b8;
|
||||||
|
border-color: #94a3b8;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
#msg {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: none;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
#msg.ok {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #047857;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#msg.err {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #b91c1c;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#msg.info {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 8px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.hint a {
|
||||||
|
color: #2563eb;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
#debug {
|
||||||
|
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #4b5563;
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
details > summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>雪球 Cookie 复制</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>xq_a_token</label>
|
||||||
|
<div id="xq-val" class="val empty">未读取</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>u</label>
|
||||||
|
<div id="u-val" class="val empty">未读取</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>组合字符串</label>
|
||||||
|
<textarea id="combined-val" class="val" readonly>未读取</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-row full">
|
||||||
|
<button id="copy-combined">复制组合字符串</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button id="copy-env" class="secondary">复制 .env 行</button>
|
||||||
|
<button id="copy-xq" class="secondary">仅 xq_a_token</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-row full">
|
||||||
|
<button id="reload-btn" class="secondary">🔄 重新读取</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="msg"></div>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>调试信息(点开看)</summary>
|
||||||
|
<div id="debug">加载中…</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>手动粘贴(兜底)</summary>
|
||||||
|
<p style="font-size: 11px; color: #6b7280; margin: 6px 0;">
|
||||||
|
如果自动读不到:打开 xueqiu.com 页面 → F12 → Network → 任意请求 → Request Headers → 复制 <code>Cookie</code> 字段整行 → 粘到下面 → 点「提取」。
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
id="manual-textarea"
|
||||||
|
class="val"
|
||||||
|
style="width: 100%; box-sizing: border-box; min-height: 60px; font-size: 11px;"
|
||||||
|
placeholder="xq_a_token=xxx; u=yyy; device_id=zzz; ..."
|
||||||
|
></textarea>
|
||||||
|
<button id="use-manual" class="secondary" style="margin-top: 6px;">从粘贴内容提取</button>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="hint">
|
||||||
|
用法:<br />
|
||||||
|
1) 在 Chrome 登录过 <a href="https://xueqiu.com/" target="_blank">xueqiu.com</a>(一次即可)<br />
|
||||||
|
2) 打开此弹窗 → 选格式 → 一键复制<br />
|
||||||
|
3) 粘到 <code>.env</code> / dashboard / 任何你需要的地方
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
// 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();
|
||||||
Reference in New Issue
Block a user