Files
dy/backend/static/help/credential-tool.html
2026-07-23 17:56:25 +08:00

1013 lines
49 KiB
HTML
Raw Permalink 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.
<!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(未抓到 WSNetwork→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_idlocalStorage 里没找到 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>