1013 lines
49 KiB
HTML
1013 lines
49 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>抖音凭证采集工具 · 本地生成 storage_state</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0f1020;
|
||
--panel: #1a1b2e;
|
||
--panel2: #23244044;
|
||
--border: #2c2e4a;
|
||
--text: #e6e6f0;
|
||
--muted: #9aa0c0;
|
||
--accent: #8b5cf6;
|
||
--accent2: #a855f7;
|
||
--ok: #34d399;
|
||
--warn: #fbbf24;
|
||
--err: #f87171;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
background: radial-gradient(1200px 600px at 70% -10%, #2a1c4a55, transparent), var(--bg);
|
||
color: var(--text);
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
|
||
line-height: 1.6;
|
||
padding: 32px 16px 80px;
|
||
}
|
||
.wrap { max-width: 920px; margin: 0 auto; }
|
||
h1 { font-size: 24px; margin: 0 0 4px; }
|
||
.sub { color: var(--muted); margin: 0 0 24px; font-size: 14px; }
|
||
.card {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
padding: 20px 22px;
|
||
margin-bottom: 18px;
|
||
}
|
||
.step-h {
|
||
display: flex; align-items: center; gap: 10px;
|
||
font-weight: 600; font-size: 16px; margin-bottom: 10px;
|
||
}
|
||
.badge {
|
||
width: 26px; height: 26px; flex: none;
|
||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||
border-radius: 50%; display: grid; place-items: center;
|
||
font-size: 14px; font-weight: 700; color: #fff;
|
||
}
|
||
p.tip { color: var(--muted); font-size: 13.5px; margin: 6px 0; }
|
||
code.k { background: #00000055; padding: 1px 6px; border-radius: 5px; color: #c4b5fd; font-size: 13px; }
|
||
textarea {
|
||
width: 100%; min-height: 92px; resize: vertical;
|
||
background: #0c0d1a; color: var(--text);
|
||
border: 1px solid var(--border); border-radius: 10px;
|
||
padding: 12px; font-family: ui-monospace, Consolas, monospace; font-size: 12.5px;
|
||
outline: none;
|
||
}
|
||
textarea:focus { border-color: var(--accent); }
|
||
.snippet {
|
||
position: relative; background: #0c0d1a; border: 1px solid var(--border);
|
||
border-radius: 10px; padding: 12px 12px; margin: 10px 0;
|
||
font-family: ui-monospace, Consolas, monospace; font-size: 12.5px;
|
||
white-space: pre-wrap; word-break: break-all; color: #a5b4fc;
|
||
}
|
||
button {
|
||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||
color: #fff; border: none; border-radius: 10px;
|
||
padding: 10px 18px; font-size: 14px; font-weight: 600; cursor: pointer;
|
||
transition: opacity .15s;
|
||
}
|
||
button:hover { opacity: .9; }
|
||
button.ghost {
|
||
background: transparent; border: 1px solid var(--border); color: var(--muted);
|
||
padding: 6px 12px; font-size: 12.5px; font-weight: 500;
|
||
}
|
||
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 12px; }
|
||
.out { min-height: 160px; }
|
||
.checks { margin-top: 12px; display: grid; gap: 6px; }
|
||
.chk { display: flex; align-items: center; gap: 8px; font-size: 13.5px; }
|
||
.dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
|
||
.ok .dot { background: var(--ok); } .ok { color: var(--ok); }
|
||
.warn .dot { background: var(--warn); } .warn { color: var(--warn); }
|
||
.err .dot { background: var(--err); } .err { color: var(--err); }
|
||
.hint-box {
|
||
background: #1e1b3a; border: 1px solid #3b2f6b; border-radius: 10px;
|
||
padding: 10px 14px; margin: 10px 0; font-size: 13px; color: #cbd5e1;
|
||
}
|
||
ol { margin: 6px 0 6px 4px; padding-left: 20px; }
|
||
ol li { margin: 4px 0; font-size: 13.5px; }
|
||
.pill { font-size: 11px; color: var(--muted); border: 1px solid var(--border); padding: 1px 8px; border-radius: 999px; }
|
||
.toast {
|
||
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
||
background: #111; border: 1px solid var(--accent); color: #fff;
|
||
padding: 10px 18px; border-radius: 10px; opacity: 0; transition: opacity .2s; pointer-events: none;
|
||
font-size: 13.5px;
|
||
}
|
||
.toast.show { opacity: 1; }
|
||
.toast.warn { border-color: #fbbf24; background: #2b231c; max-width: min(92vw, 520px); text-align: left; line-height: 1.5; }
|
||
.ws-tag { font-size: 11px; color: #6ee7b7; border: 1px solid #2f5b39; background: #1c2b1e; padding: 2px 8px; border-radius: 999px; margin-left: 6px; }
|
||
textarea.auto-filled { border-color: #34d399 !important; box-shadow: 0 0 0 2px rgba(52, 211, 153, 0.2); }
|
||
.ws-missing-hint {
|
||
background: #2b231c; border: 1px solid #b45309; border-radius: 10px;
|
||
padding: 12px 14px; margin-top: 12px; font-size: 13px; color: #fcd34d;
|
||
}
|
||
.ws-missing-hint ul { margin: 8px 0 0; padding-left: 18px; color: #fde68a; }
|
||
.ws-missing-hint li { margin: 4px 0; }
|
||
.tabs-nav {
|
||
display: flex; gap: 8px; margin-bottom: 20px;
|
||
border-bottom: 1px solid var(--border); padding-bottom: 8px; flex-wrap: wrap;
|
||
}
|
||
.tab-btn {
|
||
background: transparent; border: 1px solid transparent; color: var(--muted);
|
||
padding: 10px 16px; font-size: 14px; font-weight: 600; cursor: pointer;
|
||
transition: all 0.25s; display: flex; align-items: center; gap: 8px;
|
||
border-radius: 10px; outline: none;
|
||
}
|
||
.tab-btn:hover { color: var(--text); background: var(--panel2); }
|
||
.tab-btn.active {
|
||
color: #fff; background: var(--panel2); border-color: var(--border);
|
||
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.15);
|
||
}
|
||
.tab-num {
|
||
width: 22px; height: 22px; border-radius: 50%; background: var(--border);
|
||
display: grid; place-items: center; font-size: 12px; font-weight: 700;
|
||
color: var(--muted); transition: all 0.25s;
|
||
}
|
||
.tab-btn.active .tab-num {
|
||
background: linear-gradient(135deg, var(--accent), var(--accent2)); color: #fff;
|
||
}
|
||
.tab-pane { display: none; }
|
||
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||
.tab-pane.active { display: block; animation: fadeIn 0.3s ease-out forwards; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<h1>抖音凭证采集工具 <span class="pill">本地运行 · 不上传任何数据</span></h1>
|
||
<p class="sub">
|
||
在你<strong>本地电脑</strong>的浏览器里登录抖音网页版,按下面步骤采集凭证,生成可直接导入系统的 JSON。
|
||
解决服务器(Linux/无界面)扫码登录弹不出二维码的问题。
|
||
</p>
|
||
|
||
<div class="tabs-nav">
|
||
<button id="tab-btn-1" class="tab-btn active" onclick="switchTab(1)">
|
||
<span class="tab-num">1</span><span class="tab-txt">1. 登录网页版</span>
|
||
</button>
|
||
<button id="tab-btn-2" class="tab-btn" onclick="switchTab(2)">
|
||
<span class="tab-num">2</span><span class="tab-txt">2. 浏览器插件采集 (推荐)</span>
|
||
</button>
|
||
<button id="tab-btn-3" class="tab-btn" onclick="switchTab(3)">
|
||
<span class="tab-num">3</span><span class="tab-txt">3. 手动复制 Cookie (备用)</span>
|
||
</button>
|
||
<button id="tab-btn-4" class="tab-btn" onclick="switchTab(4)">
|
||
<span class="tab-num">4</span><span class="tab-txt">4. 采集与生成</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- TAB 1 -->
|
||
<div id="tab-pane-1" class="tab-pane active">
|
||
<div class="card">
|
||
<div class="step-h"><span class="badge">1</span> 登录抖音网页版</div>
|
||
<p class="tip">
|
||
用本地 Chrome / Edge 打开 <code class="k">https://www.douyin.com</code> 并<strong>扫码登录</strong>,
|
||
然后<strong>打开一次「私信」页面</strong>(让安全 SDK 写入签名凭证)。保持该标签页打开。
|
||
</p>
|
||
</div>
|
||
<div class="row" style="margin-top:20px; justify-content: flex-end;">
|
||
<button onclick="switchTab(2)">下一步:插件采集</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB 2 -->
|
||
<div id="tab-pane-2" class="tab-pane">
|
||
<div class="card">
|
||
<div class="step-h"><span class="badge">2</span> 使用独立浏览器插件(一键获取所有凭证)</div>
|
||
<p class="tip">
|
||
由于安全限制,抖音网页版的 <code class="k">sessionid</code> 是 HttpOnly 的,普通脚本无法直接读取。
|
||
我们提供极简的<strong>独立浏览器插件</strong>,加载后会在抖音网页右下角生成一键采集按钮,自动提取包含 HttpOnly 在内的所有凭证并打包复制。
|
||
</p>
|
||
<div class="hint-box" style="background:#1e1b3a; border-color:#3b2f6b; padding:16px;">
|
||
<strong style="color: var(--ok); display: block; margin-bottom: 6px;">💡 下载插件文件到本地</strong>
|
||
<p class="tip" style="margin-top:4px; color: var(--text); font-weight: 500;">
|
||
在本地新建文件夹(例如 <code class="k">douyin-plugin</code>),依次点击下方三个按钮下载文件并保存到该文件夹:
|
||
</p>
|
||
<div class="row" style="margin-top:12px; gap:12px;">
|
||
<button onclick="downloadPluginFile('manifest.json')">1. 下载 manifest.json</button>
|
||
<button onclick="downloadPluginFile('background.js')">2. 下载 background.js</button>
|
||
<button onclick="downloadPluginFile('content.js')">3. 下载 content.js</button>
|
||
</div>
|
||
</div>
|
||
<ol style="margin-top: 16px;">
|
||
<li>打开 Chrome 或 Edge,在地址栏输入 <code class="k">chrome://extensions/</code>(或 <code class="k">edge://extensions/</code>)并回车。</li>
|
||
<li>在扩展程序页面右上角,开启<strong>「开发者模式」</strong>开关。</li>
|
||
<li>点击左上角<strong>「加载已解压的扩展程序」</strong> (Load unpacked),选择刚才保存 3 个文件的 <code class="k">douyin-plugin</code> 文件夹。</li>
|
||
<li>刷新<strong>已登录抖音的网页标签页</strong>,页面右下角会出现 <code class="k">📌 一键采集凭证</code> 按钮,点击它。</li>
|
||
<li>凭证将被自动采集并复制到剪贴板,随后直接粘贴到「步骤 4:采集与生成」的输入框即可。</li>
|
||
</ol>
|
||
<div class="hint-box" style="background:#1c2b1e; border-color:#2f5b39; margin-top:14px;">
|
||
<strong style="color:var(--ok);">✓ 安全与私密保证</strong>:该插件完全运行在你的本地浏览器,无任何外部连接,纯本地提取。
|
||
</div>
|
||
</div>
|
||
<div class="row" style="margin-top:20px; justify-content: space-between;">
|
||
<button class="ghost" onclick="switchTab(1)">上一步</button>
|
||
<button onclick="switchTab(4)">下一步:采集与生成</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB 3 -->
|
||
<div id="tab-pane-3" class="tab-pane">
|
||
<div class="card">
|
||
<div class="step-h"><span class="badge">3</span> 备用方案:手动从网络请求头复制 Cookie</div>
|
||
<p class="tip">
|
||
若不方便安装浏览器插件,可手动从开发者工具(F12)的网络请求头复制包含 sessionid 的完整 Cookie:
|
||
</p>
|
||
<ol>
|
||
<li>在抖音页面按 <code class="k">F12</code> → 切到 <strong>Network(网络)</strong> 面板</li>
|
||
<li>在 Filter 过滤框输入 <code class="k">user_detail</code> 或 <code class="k">douyin.com</code> 并刷新页面</li>
|
||
<li>在请求列表里点击对应的请求(如 <code class="k">user_detail/?aid=...</code>)</li>
|
||
<li>找到右侧 <strong>Request Headers(请求标头)</strong> 里的 <code class="k">Cookie:</code> 这一行</li>
|
||
<li>把 <code class="k">Cookie:</code> 后面<strong>整段值</strong>复制(或右键该请求 → Copy → Copy request headers),粘贴到下面:</li>
|
||
</ol>
|
||
<div class="hint-box">
|
||
粘贴内容应类似:<code class="k">ttwid=1%7C...; sessionid=xxxx; sessionid_ss=xxxx; sid_guard=...; uid_tt=...; ...</code>
|
||
—— 必须包含 <strong>sessionid</strong>。
|
||
</div>
|
||
<textarea id="cookieInput" placeholder="在此粘贴完整 Cookie 请求头的值,或整段 Request Headers(工具会自动从中提取 Cookie)"></textarea>
|
||
</div>
|
||
<div class="row" style="margin-top:20px; justify-content: space-between;">
|
||
<button class="ghost" onclick="switchTab(2)">上一步</button>
|
||
<button onclick="switchTab(4)">下一步:采集与生成</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB 4 -->
|
||
<div id="tab-pane-4" class="tab-pane">
|
||
<div class="card">
|
||
<div class="step-h"><span class="badge">4</span> 采集与生成</div>
|
||
<p class="tip">
|
||
将<strong>插件采集的 <code class="k">DYCRED1.</code> 密文</strong>,或<strong>书签/控制台导出的 localStorage JSON</strong> 粘贴到下方输入框:
|
||
</p>
|
||
<div class="hint-box" style="background:#1c2b1e;border-color:#2f5b39;">
|
||
<strong>方式 B:书签小工具(localStorage 辅助采集):</strong>
|
||
把下面按钮<strong>拖到浏览器书签栏</strong>。在抖音私信页点击,结果复制到剪贴板后粘贴到下方即可(WS 会自动填入步骤 ★)。
|
||
<div class="row" style="margin-top:10px;">
|
||
<a id="bookmarklet" class="pill" style="padding:8px 16px;color:#fff;background:linear-gradient(135deg,var(--accent),var(--accent2));text-decoration:none;cursor:grab;">📌 抖音凭证采集(拖我到书签栏)</a>
|
||
</div>
|
||
</div>
|
||
<details style="margin-top: 16px; border: 1px dashed var(--border); border-radius: 10px; padding: 12px 14px; background: rgba(0,0,0,0.15);">
|
||
<summary style="font-size: 13.5px; font-weight: 600; cursor: pointer; color: var(--muted); outline: none; user-select: none;">
|
||
💡 方式 C:使用浏览器控制台采集(点击展开 / 收起)
|
||
</summary>
|
||
<p class="tip" style="margin-top:10px;">
|
||
切到 <strong>Console(控制台)</strong>,粘贴下面这段并回车(会自动复制结果到剪贴板)。
|
||
</p>
|
||
<div class="snippet" id="snippet" style="max-height: 150px; overflow-y: auto;"></div>
|
||
<div class="row">
|
||
<button class="ghost" onclick="copyText(document.getElementById('snippet').innerText,'已复制采集脚本')">复制采集脚本</button>
|
||
</div>
|
||
</details>
|
||
<p class="tip">把采集到的内容(<code class="k">DYCRED1.</code> 密文或 JSON)粘贴到下面,工具会自动识别并生成:</p>
|
||
<textarea id="lsInput" placeholder='在此粘贴采集到的内容(DYCRED1.xxx 密文,或 {"localStorage":{...}} JSON)'></textarea>
|
||
<div id="wsParseHint" class="ws-missing-hint" style="display:none;">
|
||
<strong>⚠️ 未识别到 WebSocket 连接</strong>
|
||
<ul>
|
||
<li>书签结果里没有 <code class="k">frontier_ws_url</code>,请在抖音<strong>私信页</strong>:F12 → Network → 过滤 <code class="k">WS</code> → 找到 <code class="k">frontier</code> / <code class="k">ws/v2</code> → 右键 Copy URL</li>
|
||
<li>将复制的 <code class="k">wss://...</code> 粘贴到<strong>本框末尾换行</strong>,或直接粘贴到下方「步骤 ★」</li>
|
||
<li>或在私信页<strong>先点书签</strong> → 刷新私信页 → WS 连接后再<strong>点一次书签</strong>重新采集</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="step-h"><span class="badge" style="background:linear-gradient(135deg,#e08c2f,#e0492f);">★</span> 私信 WebSocket 连接 <span class="ws-tag">脚本可自动抓取</span></div>
|
||
<div class="hint-box" style="background:#2b231c;border-color:#5b462f;">
|
||
下方内容由<strong>步骤 4 粘贴内容时自动识别填入</strong>。若未识别到,可手动:F12 → Network → WS → 找到 <code class="k">frontier</code> → Copy URL 并粘贴。
|
||
</div>
|
||
<textarea id="wsInput" placeholder="粘贴 wss://frontier-im.douyin.com/ws/v2?token=...&sdk_cert=...&ts_sign=... 完整 URL" style="min-height:90px; margin-bottom:12px;"></textarea>
|
||
<p class="tip"><strong>抖音数字 UID:</strong>(建议填写,以规避云端 IP 查询风控)</p>
|
||
<input type="text" id="uidInput" placeholder="例如:869032150442612(一键采集会自动填充)" style="width:100%;height:38px;background:#0c0d1a;color:var(--text);border:1px solid var(--border);border-radius:10px;padding:0 12px;outline:none;font-size:13.5px;transition:border-color .15s;" />
|
||
</div>
|
||
<div id="step4Card" class="card">
|
||
<div class="step-h"><span class="badge">4</span> 生成并导入</div>
|
||
<div class="row">
|
||
<button onclick="generate()">生成凭证 JSON</button>
|
||
<button class="ghost" onclick="copyText(document.getElementById('output').value,'已复制凭证 JSON')">复制结果</button>
|
||
</div>
|
||
<div class="checks" id="checks"></div>
|
||
<textarea id="output" class="out" placeholder="生成的 storage_state JSON 会出现在这里" style="margin-top:12px;"></textarea>
|
||
<div class="hint-box" style="margin-top:14px;">
|
||
<strong>导入步骤:</strong>回到系统 → 账号管理 → 对应账号点「编辑」→ 把上面的 JSON 粘贴进 <strong>Cookie</strong> 框 → 保存 →
|
||
再点「启动托管」。系统会直接 IM 直连,不再需要在服务器上扫码。
|
||
</div>
|
||
</div>
|
||
<div class="row" style="margin-top:20px;">
|
||
<button class="ghost" onclick="switchTab(3)">上一步</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
function switchTab(idx) {
|
||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active'));
|
||
document.getElementById('tab-btn-' + idx)?.classList.add('active');
|
||
document.getElementById('tab-pane-' + idx)?.classList.add('active');
|
||
}
|
||
|
||
const LS_KEYS = [
|
||
"security-sdk/s_sdk_crypt_sdk",
|
||
"security-sdk/s_sdk_sign_data_key/web_protect",
|
||
"tea_cache_tokens",
|
||
"web_runtime_security_uid"
|
||
];
|
||
|
||
const WS_HOOK_CODE = `(function(){
|
||
function __dyIsImWs(u){
|
||
u=String(u||'');
|
||
return u&&/\\/ws\\/v2/i.test(u)&&/[?&]token=/.test(u)&&(/frontier/i.test(u)||/zijieapi\\.com/i.test(u));
|
||
}
|
||
function __dySaveWs(u){
|
||
if(!__dyIsImWs(u))return;
|
||
var a=[];try{a=JSON.parse(sessionStorage.getItem('__dy_captured_ws')||'[]');}catch(e){}
|
||
if(a.indexOf(u)<0)a.unshift(u);
|
||
sessionStorage.setItem('__dy_captured_ws',JSON.stringify(a.slice(0,5)));
|
||
}
|
||
if(window.__dyWsHookInstalled)return;
|
||
window.__dyWsHookInstalled=1;
|
||
var O=window.WebSocket;
|
||
if(!O)return;
|
||
try{
|
||
var origSend=O.prototype.send;
|
||
O.prototype.send=function(){try{__dySaveWs(this.url);}catch(e){}return origSend.apply(this,arguments);};
|
||
}catch(e){}
|
||
function W(u,p){try{__dySaveWs(String(u||''));}catch(e){}return arguments.length>1?new O(u,p):new O(u);}
|
||
W.prototype=O.prototype;
|
||
Object.keys(O).forEach(function(k){try{W[k]=O[k];}catch(e){}});
|
||
['CONNECTING','OPEN','CLOSING','CLOSED'].forEach(function(k){if(k in O)W[k]=O[k];});
|
||
window.WebSocket=W;
|
||
try{
|
||
new PerformanceObserver(function(list){
|
||
list.getEntries().forEach(function(e){__dySaveWs(e.name);});
|
||
}).observe({entryTypes:['resource']});
|
||
}catch(e){}
|
||
})();`;
|
||
|
||
const WS_COLLECT_CODE = `(function(){
|
||
function __dyIsImWs(u){
|
||
u=String(u||'');
|
||
return u&&/\\/ws\\/v2/i.test(u)&&/[?&]token=/.test(u)&&(/frontier/i.test(u)||/zijieapi\\.com/i.test(u));
|
||
}
|
||
function __dyCollectWsUrls(){
|
||
var found=[];
|
||
function add(u){
|
||
u=String(u||'');
|
||
if(!__dyIsImWs(u))return;
|
||
if(found.indexOf(u)<0)found.unshift(u);
|
||
}
|
||
try{(performance.getEntries()||[]).forEach(function(e){add(e.name);});}catch(e){}
|
||
try{
|
||
var stored=JSON.parse(sessionStorage.getItem('__dy_captured_ws')||'[]');
|
||
if(Array.isArray(stored))stored.forEach(add);
|
||
}catch(e){}
|
||
try{sessionStorage.setItem('__dy_captured_ws',JSON.stringify(found.slice(0,5)));}catch(e){}
|
||
return found;
|
||
}
|
||
window.__dyCollectWsUrls=__dyCollectWsUrls;
|
||
window.__dyIsImWs=__dyIsImWs;
|
||
return __dyCollectWsUrls();
|
||
})();`;
|
||
|
||
function showWsMissingHint(show) {
|
||
const el = document.getElementById('wsParseHint');
|
||
if (el) el.style.display = show ? 'block' : 'none';
|
||
}
|
||
|
||
function looksLikeBookmarkDump(raw) {
|
||
const text = (raw || '').trim();
|
||
if (text.length < 80) return false;
|
||
return text.includes('"localStorage"') || text.includes("'localStorage'") || (text.startsWith('{') && text.includes('cookie'));
|
||
}
|
||
|
||
function applyDumpToForm(d, raw, forceOverwrite) {
|
||
const ws = resolveWsFromDump(d, raw);
|
||
const wsEl = document.getElementById('wsInput');
|
||
const uidEl = document.getElementById('uidInput');
|
||
const cookieEl = document.getElementById('cookieInput');
|
||
const existingWs = (wsEl.value || '').trim();
|
||
|
||
if (d && d.cookie) {
|
||
if (forceOverwrite || !cookieEl.value.trim()) cookieEl.value = d.cookie;
|
||
}
|
||
if (d && d.my_uid) {
|
||
if (forceOverwrite || !uidEl.value.trim()) uidEl.value = d.my_uid;
|
||
}
|
||
if (ws) {
|
||
showWsMissingHint(false);
|
||
const uidFromWs = uidFromWsUrl(ws);
|
||
if (uidFromWs && (forceOverwrite || !uidEl.value.trim())) uidEl.value = uidFromWs;
|
||
if (forceOverwrite || wsEl.value !== ws) {
|
||
const isChanged = wsEl.value !== ws;
|
||
wsEl.value = ws;
|
||
if (isChanged) {
|
||
wsEl.classList.add('auto-filled');
|
||
setTimeout(() => wsEl.classList.remove('auto-filled'), 1800);
|
||
toast('已自动识别 WebSocket 并填入步骤 ★');
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
if (existingWs && isImWsUrl(existingWs)) {
|
||
showWsMissingHint(false);
|
||
return true;
|
||
}
|
||
if (looksLikeBookmarkDump(raw)) {
|
||
showWsMissingHint(true);
|
||
toastWarn('未识别到 WebSocket:脚本结果不含 WS 连接,请查看步骤 4 下方提示');
|
||
} else {
|
||
showWsMissingHint(false);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
const SNIPPET_CODE = `(function(){
|
||
${WS_HOOK_CODE}
|
||
${WS_COLLECT_CODE}
|
||
var o = { localStorage: Object.assign({}, localStorage), cookie: document.cookie, my_uid: '' };
|
||
try{
|
||
var wsArr=JSON.parse(sessionStorage.getItem('__dy_captured_ws')||'[]');
|
||
if(wsArr&&wsArr.length){ o.ws_urls=wsArr; o.frontier_ws_url=wsArr[0]; }
|
||
}catch(e){}
|
||
var wsTip = o.frontier_ws_url ? ('\\n已抓取 WebSocket: ' + o.frontier_ws_url.slice(0,72) + '...') : '\\n未抓到 WS:请刷新私信页后重新运行;或把 Network 里复制的 wss URL 追加粘贴到采集工具';
|
||
var s = JSON.stringify(o);
|
||
try { copy(s); console.log("已复制到剪贴板" + wsTip); } catch(e){ console.log(s); }
|
||
var div = document.createElement('div');
|
||
div.style = "position:fixed;top:15%;left:50%;transform:translateX(-50%);z-index:999999;background:#1a1b2e;color:#e6e6f0;padding:20px;border:2px solid #8b5cf6;border-radius:14px;box-shadow:0 10px 30px rgba(0,0,0,0.8);width:90%;max-width:550px;font-family:sans-serif;text-align:left;line-height:1.5;";
|
||
div.innerHTML = "<h3 style='margin:0 0 8px;color:#a855f7;font-size:16px;'>📌 控制台凭证采集成功</h3>" +
|
||
"<p style='margin:0 0 12px;font-size:13px;color:#9aa0c0;'>已安装 WS 监听并尝试自动复制。" + wsTip.replace(/\\n/g,'<br/>') + "</p>" +
|
||
"<textarea id='dy_copy_area' style='width:100%;height:140px;background:#0c0d1a;color:#cbd5e1;border:1px solid #2c2e4a;border-radius:8px;padding:8px;font-family:monospace;font-size:12px;outline:none;resize:none;' readonly>" + s + "</textarea>" +
|
||
"<div style='margin-top:14px;display:flex;justify-content:flex-end;gap:10px;'>" +
|
||
"<button id='dy_btn_copy' style='background:#8b5cf6;color:#fff;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font-weight:600;font-size:13px;'>复制文本</button>" +
|
||
"<button id='dy_btn_close' style='background:transparent;color:#9aa0c0;border:1px solid #2c2e4a;border-radius:8px;padding:8px 16px;cursor:pointer;font-size:13px;'>关闭</button>" +
|
||
"</div>";
|
||
document.body.appendChild(div);
|
||
var txt = document.getElementById('dy_copy_area');
|
||
document.getElementById('dy_btn_copy').onclick = function(){
|
||
txt.select(); document.execCommand('copy');
|
||
this.innerText = '已复制!';
|
||
setTimeout(function(){ document.getElementById('dy_btn_copy').innerText = '复制文本'; }, 1000);
|
||
};
|
||
document.getElementById('dy_btn_close').onclick = function(){ div.remove(); };
|
||
})()`;
|
||
document.getElementById('snippet').innerText = SNIPPET_CODE;
|
||
|
||
const BOOKMARKLET_CODE = `(function(){
|
||
${WS_HOOK_CODE}
|
||
${WS_COLLECT_CODE}
|
||
var o = { localStorage: Object.assign({}, localStorage), cookie: document.cookie, my_uid: '' };
|
||
try{
|
||
var wsArr=JSON.parse(sessionStorage.getItem('__dy_captured_ws')||'[]');
|
||
if(wsArr&&wsArr.length){ o.ws_urls=wsArr; o.frontier_ws_url=wsArr[0]; }
|
||
}catch(e){}
|
||
function refreshWs(){
|
||
try{
|
||
if(window.__dyCollectWsUrls)window.__dyCollectWsUrls();
|
||
var wsArr=JSON.parse(sessionStorage.getItem('__dy_captured_ws')||'[]');
|
||
if(wsArr&&wsArr.length){ o.ws_urls=wsArr; o.frontier_ws_url=wsArr[0]; }
|
||
else { delete o.ws_urls; o.frontier_ws_url=''; }
|
||
}catch(e){}
|
||
}
|
||
function done(){
|
||
refreshWs();
|
||
var s = JSON.stringify(o);
|
||
var tip = o.my_uid ? ('\\n(已自动提取到抖音数字 UID: ' + o.my_uid + ')') : '\\n(未获取到 UID,可在采集工具中手动输入)';
|
||
var wsTip = o.frontier_ws_url ? '\\n(已抓取 WebSocket 私信连接)' : '\\n(未抓到 WS:Network→WS→Copy URL,粘贴到采集工具步骤 ★)';
|
||
tip += wsTip;
|
||
try { navigator.clipboard.writeText(s); } catch(e){}
|
||
var div = document.createElement('div');
|
||
div.style = "position:fixed;top:15%;left:50%;transform:translateX(-50%);z-index:999999;background:#1a1b2e;color:#e6e6f0;padding:20px;border:2px solid #8b5cf6;border-radius:14px;box-shadow:0 10px 30px rgba(0,0,0,0.8);width:90%;max-width:550px;font-family:sans-serif;text-align:left;line-height:1.5;";
|
||
div.innerHTML = "<h3 style='margin:0 0 8px;color:#a855f7;font-size:16px;'>📌 抖音凭证采集成功</h3>" +
|
||
"<p style='margin:0 0 12px;font-size:13px;color:#9aa0c0;'>已尝试自动复制" + tip.replace(/\\n/g,'<br/>') + "。若粘贴失败,请点击下方按钮复制:</p>" +
|
||
"<textarea id='dy_copy_area' style='width:100%;height:140px;background:#0c0d1a;color:#cbd5e1;border:1px solid #2c2e4a;border-radius:8px;padding:8px;font-family:monospace;font-size:12px;outline:none;resize:none;' readonly>" + s + "</textarea>" +
|
||
"<div style='margin-top:14px;display:flex;justify-content:flex-end;gap:10px;'>" +
|
||
"<button id='dy_btn_copy' style='background:#8b5cf6;color:#fff;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font-weight:600;font-size:13px;'>复制文本</button>" +
|
||
"<button id='dy_btn_close' style='background:transparent;color:#9aa0c0;border:1px solid #2c2e4a;border-radius:8px;padding:8px 16px;cursor:pointer;font-size:13px;'>关闭</button>" +
|
||
"</div>";
|
||
document.body.appendChild(div);
|
||
var txt = document.getElementById('dy_copy_area');
|
||
document.getElementById('dy_btn_copy').onclick = function(){
|
||
txt.select(); document.execCommand('copy');
|
||
this.innerText = '已复制!';
|
||
setTimeout(function(){ document.getElementById('dy_btn_copy').innerText = '复制文本'; }, 1000);
|
||
};
|
||
document.getElementById('dy_btn_close').onclick = function(){ div.remove(); };
|
||
}
|
||
var fetchedUid = false, elapsed = 0, maxWait = 3500, interval = 200;
|
||
function tryFinish() {
|
||
refreshWs();
|
||
if ((o.frontier_ws_url && fetchedUid) || elapsed >= maxWait) done();
|
||
else { elapsed += interval; setTimeout(tryFinish, interval); }
|
||
}
|
||
fetch('/aweme/v1/web/query/user/')
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(data){ if(data && data.user_uid) o.my_uid = String(data.user_uid); fetchedUid = true; })
|
||
.catch(function(){
|
||
fetch('/aweme/v1/creator/user/info/')
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(data){ if(data && data.creator_user_id) o.my_uid = String(data.creator_user_id); fetchedUid = true; })
|
||
.catch(function(){ fetchedUid = true; });
|
||
});
|
||
setTimeout(tryFinish, 0);
|
||
})();`;
|
||
document.getElementById('bookmarklet').setAttribute('href', 'javascript:' + encodeURIComponent(BOOKMARKLET_CODE.replace(/\s+/g, ' ')));
|
||
|
||
const MANIFEST_JSON = `{
|
||
"manifest_version": 3,
|
||
"name": "抖音凭证助手",
|
||
"version": "1.0",
|
||
"description": "独立获取抖音免扫码凭证(含 HttpOnly sessionid、web_protect、keys 和 WebSocket 连接)",
|
||
"permissions": ["cookies"],
|
||
"host_permissions": ["https://*.douyin.com/*"],
|
||
"background": { "service_worker": "background.js" },
|
||
"content_scripts": [{
|
||
"matches": ["https://*.douyin.com/*"],
|
||
"js": ["content.js"],
|
||
"run_at": "document_start"
|
||
}]
|
||
}`;
|
||
|
||
const BACKGROUND_JS = [
|
||
'chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {',
|
||
' if (message.action === "get_cookies") {',
|
||
' chrome.cookies.getAll({ url: sender.tab.url }, (cookies) => {',
|
||
' if (chrome.runtime.lastError) {',
|
||
' sendResponse({ error: chrome.runtime.lastError.message });',
|
||
' } else {',
|
||
' const cookieStr = cookies.map(c => c.name + "=" + c.value).join("; ");',
|
||
' sendResponse({ cookie: cookieStr });',
|
||
' }',
|
||
' });',
|
||
' return true;',
|
||
' }',
|
||
'});'
|
||
].join('\n');
|
||
|
||
const CONTENT_JS = [
|
||
'(function() {',
|
||
" 'use strict';",
|
||
' const WS_HOOK = `(function(){',
|
||
' function __dyIsImWs(u){u=String(u||"");return u&&/\\\\/ws\\\\/v2/i.test(u)&&/[?&]token=/.test(u)&&(/frontier/i.test(u)||/zijieapi\\\\.com/i.test(u));}',
|
||
' function __dySaveWs(u){if(!__dyIsImWs(u))return;var a=[];try{a=JSON.parse(sessionStorage.getItem("__dy_captured_ws")||"[]");}catch(e){}if(a.indexOf(u)<0)a.unshift(u);sessionStorage.setItem("__dy_captured_ws",JSON.stringify(a.slice(0,5)));}',
|
||
' if(window.__dyWsHookInstalled)return;window.__dyWsHookInstalled=1;var O=window.WebSocket;if(!O)return;',
|
||
' try{var origSend=O.prototype.send;O.prototype.send=function(){try{__dySaveWs(this.url);}catch(e){}return origSend.apply(this,arguments);};}catch(e){}',
|
||
' function W(u,p){try{__dySaveWs(String(u||""));}catch(e){}return arguments.length>1?new O(u,p):new O(u);}',
|
||
' W.prototype=O.prototype;Object.keys(O).forEach(function(k){try{W[k]=O[k];}catch(e){}});',
|
||
' ["CONNECTING","OPEN","CLOSING","CLOSED"].forEach(function(k){if(k in O)W[k]=O[k];});window.WebSocket=W;',
|
||
' try{new PerformanceObserver(function(list){list.getEntries().forEach(function(e){__dySaveWs(e.name);});}).observe({entryTypes:["resource"]});}catch(e){}',
|
||
' })();`;',
|
||
' const script = document.createElement("script");',
|
||
' script.textContent = WS_HOOK;',
|
||
' (document.head || document.documentElement).appendChild(script);',
|
||
' script.remove();',
|
||
' if (document.readyState === "loading") window.addEventListener("load", initButton); else initButton();',
|
||
' function initButton() {',
|
||
' if (document.getElementById("dy-credential-grabber-btn")) return;',
|
||
' var btn = document.createElement("div");',
|
||
' btn.id = "dy-credential-grabber-btn";',
|
||
' btn.innerHTML = "📌 一键采集凭证";',
|
||
' btn.style = "position:fixed;bottom:80px;right:24px;z-index:999999;background:linear-gradient(135deg,#8b5cf6,#a855f7);color:#fff;padding:12px 18px;border-radius:10px;font-weight:600;font-size:14px;cursor:pointer;box-shadow:0 4px 15px rgba(139,92,246,0.4);transition:all 0.2s;user-select:none;font-family:sans-serif;";',
|
||
' btn.addEventListener("click", grabCredentials);',
|
||
' document.body.appendChild(btn);',
|
||
' }',
|
||
' function grabCredentials() {',
|
||
' var btn = document.getElementById("dy-credential-grabber-btn");',
|
||
' if (btn) { btn.innerHTML = "⏳ 正在采集..."; btn.style.background = "#4b5563"; }',
|
||
' var ls = {}; for (var i = 0; i < localStorage.length; i++) { var k = localStorage.key(i); ls[k] = localStorage.getItem(k); }',
|
||
' var keys = "", web_protect = "", sec_uid = "", uid = "";',
|
||
' for (var key in ls) {',
|
||
' var lk = key.toLowerCase();',
|
||
' if (lk.indexOf("crypt_sdk") >= 0) keys = ls[key];',
|
||
' if (lk.indexOf("web_protect") >= 0) web_protect = ls[key];',
|
||
' if (lk.indexOf("security_uid") >= 0 || lk.indexOf("runtime_security_uid") >= 0) sec_uid = ls[key];',
|
||
' if (lk.indexOf("tea_cache_tokens") >= 0) { try { var parsed = JSON.parse(ls[key]); uid = String(parsed.user_unique_id || parsed.web_id || ""); } catch(e) {} }',
|
||
' }',
|
||
' if (!sec_uid && uid) sec_uid = uid;',
|
||
' var capturedWs = []; try { capturedWs = JSON.parse(sessionStorage.getItem("__dy_captured_ws") || "[]"); } catch(e) {}',
|
||
' var wsUrl = capturedWs[0] || "";',
|
||
' var my_uid = uid; var fetchPromises = [];',
|
||
' if (!my_uid) {',
|
||
' fetchPromises.push(fetch("/aweme/v1/web/query/user/").then(function(r){return r.json();}).then(function(d){if(d&&d.user_uid)my_uid=String(d.user_uid);}).catch(function(){}));',
|
||
' fetchPromises.push(fetch("/aweme/v1/creator/user/info/").then(function(r){return r.json();}).then(function(d){if(d&&d.creator_user_id)my_uid=String(d.creator_user_id);}).catch(function(){}));',
|
||
' }',
|
||
' Promise.all(fetchPromises).then(function() {',
|
||
' chrome.runtime.sendMessage({ action: "get_cookies" }, function(response) {',
|
||
' if (!response || response.error) { showToast("❌ 获取 Cookie 失败: " + ((response && response.error) || "无响应"), "err"); resetBtn(); return; }',
|
||
' var data = { cookie: response.cookie, web_protect: web_protect, keys: keys, ua: navigator.userAgent, sec_uid: sec_uid, nickname: "", unique_id: my_uid || "", frontier_ws_url: wsUrl, ws_urls: capturedWs };',
|
||
' var finalResult = "DYCRED1." + btoa(unescape(encodeURIComponent(JSON.stringify(data))));',
|
||
' try { navigator.clipboard.writeText(finalResult).then(function(){ showToast("🎉 凭证采集成功!已自动复制到剪贴板。", "ok"); }).catch(function(){ fallbackCopy(finalResult); }); } catch(e) { fallbackCopy(finalResult); }',
|
||
' resetBtn();',
|
||
' });',
|
||
' });',
|
||
' }',
|
||
' function fallbackCopy(text) { var ta = document.createElement("textarea"); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); ta.remove(); showToast("🎉 凭证采集成功!已手动复制到剪贴板。", "ok"); }',
|
||
' function resetBtn() { var btn = document.getElementById("dy-credential-grabber-btn"); if (btn) { btn.innerHTML = "📌 一键采集凭证"; btn.style.background = "linear-gradient(135deg,#8b5cf6,#a855f7)"; } }',
|
||
' function showToast(msg, type) {',
|
||
' var toast = document.createElement("div");',
|
||
' toast.style = "position:fixed;top:10%;left:50%;transform:translateX(-50%);z-index:9999999;padding:14px 24px;border-radius:12px;color:#fff;font-weight:600;font-size:14px;box-shadow:0 8px 30px rgba(0,0,0,0.5);font-family:sans-serif;line-height:1.4;";',
|
||
' if (type === "ok") { toast.style.background = "#059669"; toast.style.border = "1px solid #34d399"; } else { toast.style.background = "#dc2626"; toast.style.border = "1px solid #f87171"; }',
|
||
' toast.innerHTML = msg; document.body.appendChild(toast);',
|
||
' setTimeout(function(){ toast.style.opacity = "0"; toast.style.transition = "opacity 0.4s"; setTimeout(function(){ toast.remove(); }, 400); }, 3000);',
|
||
' }',
|
||
'})();'
|
||
].join('\n');
|
||
|
||
function downloadPluginFile(filename) {
|
||
let content = '';
|
||
if (filename === 'manifest.json') content = MANIFEST_JSON;
|
||
else if (filename === 'background.js') content = BACKGROUND_JS;
|
||
else if (filename === 'content.js') content = CONTENT_JS;
|
||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
toast('已下载 ' + filename + ',请保存到你的插件目录');
|
||
}
|
||
|
||
function toast(msg) {
|
||
const t = document.getElementById('toast');
|
||
t.classList.remove('warn');
|
||
t.innerText = msg;
|
||
t.classList.add('show');
|
||
clearTimeout(t._t);
|
||
t._t = setTimeout(() => t.classList.remove('show'), 1600);
|
||
}
|
||
|
||
function toastWarn(msg) {
|
||
const t = document.getElementById('toast');
|
||
t.innerText = msg;
|
||
t.classList.add('show', 'warn');
|
||
clearTimeout(t._t);
|
||
t._t = setTimeout(() => t.classList.remove('show', 'warn'), 5000);
|
||
}
|
||
|
||
function copyText(text, msg) {
|
||
if (!text) { toast('没有可复制的内容'); return; }
|
||
navigator.clipboard.writeText(text).then(() => toast(msg || '已复制'))
|
||
.catch(() => {
|
||
const ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
document.execCommand('copy');
|
||
ta.remove();
|
||
toast(msg || '已复制');
|
||
});
|
||
}
|
||
|
||
function normalizeDump(raw) {
|
||
let cleanRaw = (raw || '').trim();
|
||
if (cleanRaw.startsWith('DYCRED1.')) {
|
||
try {
|
||
const b64 = cleanRaw.split('.', 2)[1];
|
||
const decoded = decodeURIComponent(escape(atob(b64)));
|
||
const data = JSON.parse(decoded);
|
||
const ls = {};
|
||
if (data.keys) ls['security-sdk/s_sdk_crypt_sdk'] = data.keys;
|
||
if (data.web_protect) ls['security-sdk/s_sdk_sign_data_key/web_protect'] = data.web_protect;
|
||
if (data.sec_uid) ls['web_runtime_security_uid'] = data.sec_uid;
|
||
if (data.unique_id) {
|
||
ls['tea_cache_tokens'] = JSON.stringify({ user_unique_id: data.unique_id, web_id: data.unique_id });
|
||
}
|
||
return {
|
||
ls,
|
||
cookie: data.cookie || '',
|
||
my_uid: data.unique_id || data.my_uid || '',
|
||
frontier_ws_url: data.frontier_ws_url || (data.ws_urls && data.ws_urls[0]) || '',
|
||
ws_urls: Array.isArray(data.ws_urls) ? data.ws_urls : []
|
||
};
|
||
} catch (e) {
|
||
console.error('Failed to parse DYCRED1 format:', e);
|
||
}
|
||
}
|
||
const obj = parseLocalStorage(cleanRaw);
|
||
if (obj === null) return null;
|
||
if (obj.localStorage && typeof obj.localStorage === 'object') {
|
||
return {
|
||
ls: obj.localStorage,
|
||
cookie: obj.cookie || '',
|
||
my_uid: obj.my_uid || '',
|
||
frontier_ws_url: obj.frontier_ws_url || '',
|
||
ws_urls: Array.isArray(obj.ws_urls) ? obj.ws_urls : []
|
||
};
|
||
}
|
||
return {
|
||
ls: obj,
|
||
cookie: obj.cookie || '',
|
||
my_uid: obj.my_uid || '',
|
||
frontier_ws_url: obj.frontier_ws_url || '',
|
||
ws_urls: Array.isArray(obj.ws_urls) ? obj.ws_urls : []
|
||
};
|
||
}
|
||
|
||
function pickLs(ls, exactKey, containsList) {
|
||
if (ls[exactKey]) return ls[exactKey];
|
||
for (const k in ls) {
|
||
const lk = k.toLowerCase();
|
||
if (containsList.some(c => lk.includes(c)) && ls[k]) return ls[k];
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function extractWebId(ls) {
|
||
const tea = pickLs(ls, 'tea_cache_tokens', ['tea_cache_tokens']);
|
||
if (tea) {
|
||
try {
|
||
const p = JSON.parse(tea);
|
||
const id = p.user_unique_id || p.web_id || '';
|
||
if (id) return String(id);
|
||
} catch (e) {}
|
||
}
|
||
for (const k in ls) {
|
||
const v = ls[k];
|
||
if (typeof v !== 'string') continue;
|
||
const m = v.match(/"user_unique_id"\s*:\s*"?(\d{6,})"?/);
|
||
if (m && m[1]) return m[1];
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function isImWsUrl(u) {
|
||
u = String(u || '');
|
||
if (!u || !/\/ws\/v2/i.test(u) || !/[?&]token=/.test(u)) return false;
|
||
return /frontier/i.test(u) || /zijieapi\.com/i.test(u);
|
||
}
|
||
|
||
function extractWsUrl(raw) {
|
||
if (!raw) return '';
|
||
const text = String(raw).replace(/\\\//g, '/');
|
||
const re = /wss?:\/\/[^\s"'`,)\]}]+/gi;
|
||
let m;
|
||
while ((m = re.exec(text)) !== null) {
|
||
if (isImWsUrl(m[0])) return m[0];
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function uidFromWsUrl(ws) {
|
||
if (!ws) return '';
|
||
const m = String(ws).match(/[?&]device_id=(\d{6,})/);
|
||
return m ? m[1] : '';
|
||
}
|
||
|
||
function resolveWsFromDump(d, raw) {
|
||
const candidates = [];
|
||
if (d) {
|
||
if (d.frontier_ws_url && String(d.frontier_ws_url).trim()) candidates.push(String(d.frontier_ws_url).trim());
|
||
if (Array.isArray(d.ws_urls)) d.ws_urls.forEach(u => { if (u && String(u).trim()) candidates.push(String(u).trim()); });
|
||
}
|
||
if (raw) {
|
||
candidates.push(String(raw));
|
||
candidates.push(String(raw).replace(/\\\//g, '/'));
|
||
}
|
||
for (const item of candidates) {
|
||
const url = extractWsUrl(item) || (isImWsUrl(item) ? item : '');
|
||
if (url) return url;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function qparam(url, key) {
|
||
const m = url.match(new RegExp('[?&]' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^&\\s]+)'));
|
||
if (!m) return '';
|
||
try { return decodeURIComponent(m[1]); } catch (e) { return m[1]; }
|
||
}
|
||
|
||
function parseWebProtect(raw) {
|
||
if (!raw) return null;
|
||
try {
|
||
let o = JSON.parse(String(raw));
|
||
if (typeof o === 'string') o = JSON.parse(o);
|
||
if (o && typeof o.data === 'string') {
|
||
try { o = JSON.parse(o.data); } catch (e) {}
|
||
} else if (o && typeof o.data === 'object' && o.data) {
|
||
o = o.data;
|
||
}
|
||
return (o && typeof o === 'object') ? o : null;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function extractCookieFromRaw(raw) {
|
||
raw = (raw || '').trim();
|
||
const m1 = raw.match(/['"]cookie['"]\s*:\s*['"]([^'"]+)['"]/i);
|
||
if (m1 && m1[1]) return m1[1];
|
||
const m2 = raw.match(/cookie:\s*['"]?([^'\r\n"]+)['"]?/i);
|
||
if (m2 && m2[1]) return m2[1].trim();
|
||
const m3 = raw.match(/cookie:\s*([^\r\n]+)/i);
|
||
if (m3 && m3[1]) {
|
||
let val = m3[1].trim();
|
||
val = val.replace(/^['"]|['"]$/g, '');
|
||
return val;
|
||
}
|
||
return raw;
|
||
}
|
||
|
||
function parseCookieHeader(raw) {
|
||
const out = [];
|
||
const farFuture = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 180;
|
||
raw.split(/;\s*/).forEach(pair => {
|
||
if (!pair) return;
|
||
const idx = pair.indexOf('=');
|
||
if (idx <= 0) return;
|
||
const name = pair.slice(0, idx).trim();
|
||
let value = pair.slice(idx + 1).trim();
|
||
if (!name || /[\(\)\[\]\{\}'"\n \t\\]/.test(name)) return;
|
||
out.push({ name, value, domain: '.douyin.com', path: '/', expires: farFuture, httpOnly: false, secure: true, sameSite: 'None' });
|
||
});
|
||
return out;
|
||
}
|
||
|
||
function parseLocalStorage(raw) {
|
||
raw = (raw || '').trim();
|
||
if (!raw) return {};
|
||
try { return JSON.parse(raw); } catch (e) {}
|
||
const ws = extractWsUrl(raw);
|
||
if (ws) {
|
||
const idx = raw.indexOf(ws);
|
||
if (idx > 0) {
|
||
const head = raw.slice(0, idx).trim();
|
||
try { return JSON.parse(head); } catch (e2) {}
|
||
const s = head.indexOf('{'), e = head.lastIndexOf('}');
|
||
if (s >= 0 && e > s) {
|
||
try { return JSON.parse(head.slice(s, e + 1)); } catch (e3) {}
|
||
}
|
||
}
|
||
}
|
||
const s = raw.indexOf('{'), e = raw.lastIndexOf('}');
|
||
if (s >= 0 && e > s) {
|
||
try { return JSON.parse(raw.slice(s, e + 1)); } catch (e2) {}
|
||
}
|
||
return extractWsUrl(raw) ? {} : null;
|
||
}
|
||
|
||
function generate(autoFocus) {
|
||
const cookieRaw = document.getElementById('cookieInput').value.trim();
|
||
const lsRaw = document.getElementById('lsInput').value.trim();
|
||
const checks = [];
|
||
|
||
if (!cookieRaw) {
|
||
renderChecks([{ s: 'err', t: '请先粘贴完整 Cookie(步骤 3 手动复制,或步骤 2 插件采集)' }]);
|
||
if (autoFocus) {
|
||
switchTab(3);
|
||
toastWarn('请先完成 Cookie 采集:使用插件一键采集,或在步骤 3 从 Network 复制 Cookie');
|
||
}
|
||
return;
|
||
}
|
||
|
||
const cleanCookie = extractCookieFromRaw(cookieRaw).replace(/^cookie\s*:/i, '').trim();
|
||
const cookies = parseCookieHeader(cleanCookie);
|
||
const names = new Set(cookies.map(c => c.name.toLowerCase()));
|
||
|
||
const dump = normalizeDump(lsRaw);
|
||
if (dump === null && lsRaw) {
|
||
renderChecks([{ s: 'err', t: '步骤 4 的内容解析失败,请重新复制采集结果' }]);
|
||
return;
|
||
}
|
||
const ls = (dump && dump.ls) || {};
|
||
|
||
if (dump) applyDumpToForm(dump, lsRaw, false);
|
||
|
||
const valKeys = pickLs(ls, 'security-sdk/s_sdk_crypt_sdk', ['crypt_sdk']);
|
||
const valProtect = pickLs(ls, 'security-sdk/s_sdk_sign_data_key/web_protect', ['web_protect']);
|
||
const webId = extractWebId(ls);
|
||
const deviceId = pickLs(ls, 'web_runtime_security_uid', ['runtime_security_uid', 'security_uid']) || webId;
|
||
|
||
const localStorageEntries = [];
|
||
if (valKeys) localStorageEntries.push({ name: 'security-sdk/s_sdk_crypt_sdk', value: String(valKeys) });
|
||
if (valProtect) localStorageEntries.push({ name: 'security-sdk/s_sdk_sign_data_key/web_protect', value: String(valProtect) });
|
||
const teaRaw = pickLs(ls, 'tea_cache_tokens', ['tea_cache_tokens']);
|
||
if (teaRaw) localStorageEntries.push({ name: 'tea_cache_tokens', value: String(teaRaw) });
|
||
else if (webId) localStorageEntries.push({ name: 'tea_cache_tokens', value: JSON.stringify({ user_unique_id: webId, web_id: webId }) });
|
||
if (deviceId) localStorageEntries.push({ name: 'web_runtime_security_uid', value: String(deviceId) });
|
||
|
||
const storageState = {
|
||
cookies,
|
||
origins: [{ origin: 'https://www.douyin.com', localStorage: localStorageEntries }],
|
||
user_agent: navigator.userAgent
|
||
};
|
||
|
||
const finalUid = document.getElementById('uidInput').value.trim() || (dump && dump.my_uid) || '';
|
||
if (finalUid) storageState.my_uid = parseInt(finalUid, 10);
|
||
|
||
let wsRaw = (document.getElementById('wsInput').value || '').trim();
|
||
if (!wsRaw && dump && dump.frontier_ws_url) wsRaw = dump.frontier_ws_url;
|
||
if (!wsRaw && dump && dump.ws_urls && dump.ws_urls[0]) wsRaw = dump.ws_urls[0];
|
||
|
||
let hasWs = false, wsSdkCert = '';
|
||
if (wsRaw) {
|
||
const wsUrl = extractWsUrl(wsRaw);
|
||
const isFrontier = wsUrl && /[?&]token=/.test(wsUrl) &&
|
||
(wsUrl.indexOf('frontier-im.douyin.com') !== -1 || (wsUrl.indexOf('frontier') !== -1 && wsUrl.indexOf('zijieapi.com') !== -1));
|
||
if (isFrontier) {
|
||
hasWs = true;
|
||
storageState.frontier_ws_url = wsUrl;
|
||
wsSdkCert = qparam(wsUrl, 'sdk_cert');
|
||
const wsTsSign = qparam(wsUrl, 'ts_sign');
|
||
if (wsSdkCert) storageState.sdk_cert = wsSdkCert;
|
||
if (wsTsSign) storageState.ts_sign = wsTsSign;
|
||
}
|
||
}
|
||
|
||
document.getElementById('output').value = JSON.stringify(storageState, null, 2);
|
||
|
||
const hasSession = names.has('sessionid') || names.has('sessionid_ss');
|
||
const hasUid = !!finalUid || names.has('uid_tt') || names.has('uid_tt_ss');
|
||
const hasProtect = !!valProtect;
|
||
const hasKeys = !!valKeys;
|
||
const hasWebId = !!webId;
|
||
const hasDevice = !!deviceId;
|
||
const wp = parseWebProtect(valProtect);
|
||
const hasWpTicket = !!(wp && wp.ticket);
|
||
const hasWpTs = !!(wp && wp.ts_sign);
|
||
const hasWpCert = !!(wp && (wp.client_cert || wp.sdk_cert));
|
||
|
||
checks.push(chk(hasSession, 'sessionid(登录态,必需)', '缺 sessionid!必须从步骤 3 Network 请求头复制 Cookie(含 sessionid=...),或使用插件一键采集'));
|
||
checks.push(chk(hasUid, '抖音数字 UID', '未提取到数字 UID(书签/插件会自动抓取);可手动填写,缺失时后台会联网查询'));
|
||
checks.push(chk(hasProtect, 'web_protect(签名凭证,必需)', '缺 web_protect!请在抖音「私信页」停留几秒让安全 SDK 写入后,重新采集'));
|
||
if (hasProtect) {
|
||
checks.push(chk(hasWpTicket && hasWpTs && hasWpCert, 'web_protect 内含 ticket + ts_sign + client_cert(发送必需)',
|
||
'web_protect 不完整!请在「私信页」停留 5~10 秒后重新采集;缺 client_cert/ts_sign 会导致发送 7911'));
|
||
}
|
||
checks.push(chk(hasKeys, 'keys / crypt_sdk(签名密钥,必需)', '缺 crypt_sdk!请重新采集 localStorage(确认在已登录的抖音标签页执行)'));
|
||
checks.push(chk(hasWebId, 'web_id', '缺 web_id:localStorage 里没找到 tea_cache_tokens;刷新抖音页面后重采集'));
|
||
checks.push(chk(hasDevice, 'device_id', '缺 device_id:没找到 web_runtime_security_uid;刷新抖音页面后重采集(已尝试用 web_id 兜底)'));
|
||
if (hasWs) {
|
||
checks.push({ s: wsSdkCert ? 'ok' : 'warn', t: wsSdkCert ? '已包含 frontier WebSocket(用于实时收消息)' : '已包含 frontier WebSocket,但未解析到 sdk_cert,请确认 URL 完整' });
|
||
} else {
|
||
checks.push({ s: 'warn', t: '未粘贴 frontier WS(步骤 ★):云端可能收不到实时消息,建议补上' });
|
||
}
|
||
|
||
renderChecks(checks);
|
||
const okCore = hasSession && hasProtect && hasKeys && hasWpTicket && hasWpTs && hasWpCert;
|
||
if (okCore) {
|
||
if (autoFocus) {
|
||
const tab4Btn = document.getElementById('tab-btn-4');
|
||
if (tab4Btn && !tab4Btn.classList.contains('active')) switchTab(4);
|
||
}
|
||
toast(hasWebId && hasDevice ? '生成成功,凭证齐全,可复制导入' : '核心凭证齐全可用;web_id/device_id 建议补全');
|
||
} else {
|
||
toast('关键凭证缺失,请看红色提示');
|
||
}
|
||
}
|
||
|
||
function chk(ok, okText, failText) {
|
||
if (ok) return { s: 'ok', t: '已包含 ' + okText };
|
||
const critical = /必需/.test(okText);
|
||
return { s: critical ? 'err' : 'warn', t: failText };
|
||
}
|
||
|
||
function renderChecks(list) {
|
||
document.getElementById('checks').innerHTML = list.map(c =>
|
||
`<div class="chk ${c.s}"><span class="dot"></span>${c.t}</div>`
|
||
).join('');
|
||
}
|
||
|
||
const cookieInputEl = document.getElementById('cookieInput');
|
||
cookieInputEl.addEventListener('input', () => generate(false));
|
||
cookieInputEl.addEventListener('change', () => generate(false));
|
||
cookieInputEl.addEventListener('paste', () => setTimeout(() => generate(true), 0));
|
||
|
||
const lsInputEl = document.getElementById('lsInput');
|
||
lsInputEl.addEventListener('input', () => {
|
||
const raw = lsInputEl.value;
|
||
applyDumpToForm(normalizeDump(raw), raw, false);
|
||
generate(false);
|
||
});
|
||
lsInputEl.addEventListener('change', () => {
|
||
const raw = lsInputEl.value;
|
||
applyDumpToForm(normalizeDump(raw), raw, false);
|
||
generate(false);
|
||
});
|
||
lsInputEl.addEventListener('paste', () => setTimeout(() => {
|
||
const raw = lsInputEl.value;
|
||
applyDumpToForm(normalizeDump(raw), raw, true);
|
||
generate(true);
|
||
}, 0));
|
||
|
||
const wsInputEl = document.getElementById('wsInput');
|
||
const handleWsInput = (isPaste) => {
|
||
const ws = extractWsUrl(wsInputEl.value);
|
||
if (ws) showWsMissingHint(false);
|
||
if (!ws) { generate(isPaste); return; }
|
||
const uid = uidFromWsUrl(ws);
|
||
if (uid && !document.getElementById('uidInput').value.trim()) document.getElementById('uidInput').value = uid;
|
||
generate(isPaste);
|
||
};
|
||
wsInputEl.addEventListener('input', () => handleWsInput(false));
|
||
wsInputEl.addEventListener('change', () => handleWsInput(false));
|
||
wsInputEl.addEventListener('paste', () => setTimeout(() => handleWsInput(true), 0));
|
||
|
||
document.getElementById('uidInput').addEventListener('input', () => generate(false));
|
||
document.getElementById('uidInput').addEventListener('change', () => generate(false));
|
||
</script>
|
||
</body>
|
||
</html>
|