Files
douyin/凭证采集工具.html
2026-07-17 09:24:47 +08:00

1342 lines
56 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; }
details.card summary { list-style: none; }
details.card summary::-webkit-details-marker { display: none; }
details.card[open] summary { margin-bottom: 4px; }
</style>
</head>
<body>
<div class="wrap">
<h1>抖音凭证采集工具 <span class="pill">本地运行 · 不上传任何数据</span></h1>
<p class="sub">
在你<strong>本地电脑</strong>的浏览器里登录抖音网页版,按下面步骤采集凭证,生成可直接导入系统的 JSON。
解决服务器(Linux/无界面)扫码登录弹不出二维码的问题。
</p>
<!-- STEP 1 -->
<div class="card">
<div class="step-h"><span class="badge">1</span> 登录抖音网页版</div>
<p class="tip">
用本地 Chrome / Edge 打开 <code class="k">https://creator.douyin.com/creator-micro/data/following/chat</code><strong>扫码登录</strong>
然后<strong>打开一次「私信」页面</strong>(让安全 SDK 写入签名凭证)。保持该标签页打开,进入步骤 2 点书签一键采集。
</p>
</div>
<!-- STEP 2 一键采集 -->
<div class="card" style="border-color:#5b3f8a;">
<div class="step-h"><span class="badge">2</span> 一键采集(Cookie + 签名 + WebSocket</div>
<div class="hint-box" style="background:#1c2b1e;border-color:#2f5b39;">
<strong>只需 3 步:</strong>
<ol style="margin:8px 0 0;">
<li>把下面按钮<strong>拖到浏览器书签栏</strong>(只需拖一次)</li>
<li><strong>已登录且打开过私信页</strong>的抖音标签页,<strong>点一下书签</strong></li>
<li>回到本页,在下方粘贴框 <strong>Ctrl+V</strong> —— 会自动填充步骤 2/3/★ 并生成 JSON</li>
</ol>
<div class="row" style="margin-top:12px;">
<a id="bookmarklet" class="pill" style="padding:10px 18px;color:#fff;background:linear-gradient(135deg,var(--accent),var(--accent2));text-decoration:none;cursor:grab;font-size:13px;">📌 抖音一键采集(拖我到书签栏)</a>
<button class="ghost" onclick="copyText(document.getElementById('snippet').innerText,'已复制采集脚本')">复制控制台脚本(备选)</button>
</div>
<p class="tip" style="margin:10px 0 0;">
书签会同时采集:<code class="k">sessionid</code>(完整 Cookie)、<code class="k">localStorage</code> 签名凭证、
<code class="k">frontier WS</code> 连接、数字 UID。<br/>
<strong>若缺 sessionid</strong>在下方「快速补充 sessionid」单行框粘贴值即可(比整段 Cookie 更快)。<br/>
<strong>若缺 WS</strong>先点书签 → <strong>刷新私信页</strong>(等消息列表加载出来)→ <strong>再点一次书签</strong>(第二次会读到刷新时捕获的 WS)。<br/>
<strong>若仍缺 WS</strong>在下方「快速补充 Frontier WS」粘贴 F12 → Network → WS 标签里的 URL。
</p>
</div>
<p class="tip" style="margin-top:14px;"><strong>粘贴一键采集结果(自动填充并生成):</strong></p>
<textarea id="unifiedInput" placeholder='在此粘贴:① 书签采集 JSON ② 或「启动抖音一键采集器.bat」生成的 douyin_storage_state.json 全文 — 粘贴后自动填充并生成' style="min-height:100px;"></textarea>
<p class="tip" style="margin-top:10px;"><strong>快速补充 sessionid(仅当采集后提示缺失时):</strong></p>
<div style="display:flex;gap:8px;margin-top:6px;">
<input type="text" id="sessionidInput" placeholder="粘贴 sessionid 值(从 F12 → Application → Cookies → douyin.com 复制)" style="flex:1;height:38px;background:#0c0d1a;color:var(--text);border:1px solid var(--border);border-radius:10px;padding:0 12px;outline:none;font-size:13px;" />
<button class="ghost" style="white-space:nowrap;" onclick="applySessionId()">合并</button>
</div>
<p class="tip" style="margin-top:10px;"><strong>或粘贴完整 Cookie 头(备选):</strong> F12 → Network → 任意 douyin 请求 → Request Headers → 复制 <code class="k">Cookie:</code> 后面整段</p>
<textarea id="cookieSupplement" placeholder="sessionid=xxxx; sessionid_ss=xxxx; sid_guard=...; uid_tt=...(粘贴后自动合并并重新生成)" style="min-height:56px;margin-top:6px;"></textarea>
<p class="tip" style="margin-top:12px;"><strong>快速补充 Frontier WS(仅当采集后提示缺失时):</strong></p>
<div style="display:flex;gap:8px;margin-top:6px;">
<input type="text" id="wsQuickInput" placeholder="wss://frontier-im.douyin.com/ws/v2?token=...(从 F12 → Network → WS 标签复制)" style="flex:1;height:38px;background:#0c0d1a;color:var(--text);border:1px solid var(--border);border-radius:10px;padding:0 12px;outline:none;font-size:12px;" />
<button class="ghost" style="white-space:nowrap;" onclick="applyWsUrl()">填入</button>
</div>
<div class="snippet" id="snippet" style="display:none;"></div>
</div>
<!-- STEP 3 明细(自动填充,可手动改) -->
<details class="card" style="padding:16px 22px;">
<summary style="cursor:pointer;font-weight:600;font-size:15px;color:var(--muted);">展开查看 / 手动编辑各字段(步骤 2 · 3 · ★)</summary>
<div style="margin-top:16px;">
<div class="step-h"><span class="badge">2</span> 完整 Cookie(含 sessionid</div>
<textarea id="cookieInput" placeholder="一键采集后自动填充;或手动从 Network → Request Headers → Cookie 复制" style="min-height:72px;"></textarea>
<div class="step-h" style="margin-top:18px;"><span class="badge">3</span> 安全签名凭证(localStorage</div>
<input type="text" id="uidInput" placeholder="抖音数字 UID(一键采集会自动填充)" 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;margin-bottom:10px;" />
<textarea id="lsInput" placeholder='localStorage JSON(一键采集后自动填充)' style="min-height:72px;"></textarea>
<div class="step-h" style="margin-top:18px;"><span class="badge" style="background:linear-gradient(135deg,#e08c2f,#e0492f);"></span> WebSocket 私信连接</div>
<textarea id="wsInput" placeholder="wss://frontier-im.douyin.com/ws/v2?token=...(一键采集后自动填充)" style="min-height:72px;"></textarea>
</div>
</details>
<!-- STEP 4 -->
<div 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>
<div class="toast" id="toast"></div>
<script>
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"
];
// 在抖音页面安装 WebSocket 钩子,持久化到 sessionStorage
const WS_HOOK_CODE = `(function(){
function __dyIsImWs(u){
u=String(u||'');
if(!u||!/^wss?:/i.test(u))return false;
if(!/[?&]token=/.test(u))return false;
if(/frontier/i.test(u))return true;
if(/zijieapi\\.com/i.test(u))return true;
return /\\/ws\\/v2/i.test(u)&&/douyin/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){}
try{
var desc=Object.getOwnPropertyDescriptor(O.prototype,'readyState');
if(desc&&desc.get){
var origGet=desc.get;
Object.defineProperty(O.prototype,'readyState',{
get:function(){try{__dySaveWs(this.url);}catch(e){}return origGet.apply(this,arguments);},
configurable:true,enumerable:true
});
}
}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{
var perfObs=new PerformanceObserver(function(list){
list.getEntries().forEach(function(e){__dySaveWs(e.name);});
});
perfObs.observe({entryTypes:['resource']});
// 立即扫描已有的 performance entries
try{(performance.getEntries()||[]).forEach(function(e){__dySaveWs(e.name);});}catch(e2){}
}catch(e){}
// 用 sessionStorage 持久化 hook 状态,刷新后仍可恢复
try{
sessionStorage.setItem('__dy_ws_hook_active','1');
sessionStorage.setItem('__dy_ws_hook_ts',String(Date.now()));
}catch(e){}
})();`;
const WS_COLLECT_CODE = `(function(){
function __dyIsImWs(u){
u=String(u||'');
if(!u||!/^wss?:/i.test(u))return false;
if(!/[?&]token=/.test(u))return false;
if(/frontier/i.test(u))return true;
if(/zijieapi\\.com/i.test(u))return true;
return /\\/ws\\/v2/i.test(u)&&/douyin/i.test(u);
}
function __dyScanTextForWs(text, add){
text=String(text||'');
var re=/wss?:\\/\\/[^\\s"(),\\]\\}]+/gi,m;
while((m=re.exec(text))!==null)add(m[0]);
}
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{
var si,sj,sk;
for(si=0;si<sessionStorage.length;si++){
__dyScanTextForWs(sessionStorage.getItem(sessionStorage.key(si)),add);
}
for(sj=0;sj<localStorage.length;sj++){
__dyScanTextForWs(localStorage.getItem(localStorage.key(sj)),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;
// 每 500ms 扫描一次 performance entries,持续 10 秒
var __dyScanCount=0;
var __dyScanTimer=setInterval(function(){
try{
__dyCollectWsUrls();
if(++__dyScanCount>=20) clearInterval(__dyScanTimer);
}catch(e){clearInterval(__dyScanTimer);}
},500);
return __dyCollectWsUrls();
})();`;
// 一键采集:WS 监听 + cookieStore + 轮询等待 WS
const COLLECTOR_CODE = `(function(){
${WS_HOOK_CODE}
${WS_COLLECT_CODE}
var o = {
localStorage: Object.assign({}, localStorage),
cookie: document.cookie,
cookie_header: '',
frontier_ws_url: '',
ws_urls: [],
my_uid: ''
};
function __dyExtractUidLocal(ls, cookieStr){
ls = ls || {};
cookieStr = cookieStr || '';
try{
var tea = ls['tea_cache_tokens'];
if(tea){
var p = JSON.parse(tea);
var id = p.user_unique_id || p.web_id || '';
if(id && /^\\d{5,}$/.test(String(id))) return String(id);
}
}catch(e){}
try{
var sec = ls['web_runtime_security_uid'];
if(sec && /^\\d{5,}$/.test(String(sec))) return String(sec);
}catch(e){}
for(var k in ls){
if(!Object.prototype.hasOwnProperty.call(ls,k)) continue;
var v = String(ls[k] || '');
var m = v.match(/"(?:user_uid|creator_user_id|user_unique_id|uid)"\\s*:\\s*"?([0-9]{5,})"?/);
if(m && m[1]) return m[1];
}
try{
var nodes = document.querySelectorAll('script');
for(var i=0;i<nodes.length;i++){
var t = nodes[i].textContent || '';
var m2 = t.match(/"(?:user_uid|creator_user_id)"\\s*:\\s*"?([0-9]{5,})"?/);
if(m2 && m2[1]) return m2[1];
}
}catch(e){}
return '';
}
function __dyUidFromWs(ws){
if(!ws) return '';
var m = String(ws).match(/[?&]device_id=([0-9]{6,})/);
return m ? m[1] : '';
}
function __dyPickUid(data){
if(!data || typeof data !== 'object') return '';
if(data.user_uid) return String(data.user_uid);
if(data.creator_user_id) return String(data.creator_user_id);
if(data.user && data.user.uid) return String(data.user.uid);
if(data.data){
if(data.data.user_uid) return String(data.data.user_uid);
if(data.data.creator_user_id) return String(data.data.creator_user_id);
if(data.data.user && data.data.user.uid) return String(data.data.user.uid);
}
return '';
}
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];
}
}catch(e){}
}
function collectCookies(){
var map = {};
function merge(str){
(str||'').split(';').forEach(function(pair){
var i = pair.indexOf('=');
if(i>0){
var k=pair.slice(0,i).trim(), v=pair.slice(i+1).trim();
if(k&&!/[()\\[\\]{}\\s\\\\]/.test(k)) map[k]=v;
}
});
}
function deriveSessionId(){
if(map.sessionid || map.sessionid_ss) return;
var raw = map.sid_guard || map.sid_tt || '';
if(!raw) return;
try{ raw = decodeURIComponent(raw); }catch(e){}
var sid = String(raw).split('|')[0].trim();
if(sid && /^[A-Za-z0-9_\-]{20,}$/.test(sid)){
map.sessionid = sid;
map.sessionid_ss = sid;
}
}
function saveSessionId(v){
v = String(v || '').trim();
if(!v) return;
try{ v = decodeURIComponent(v); }catch(e){}
v = v.split('|')[0].trim();
if(/^[A-Za-z0-9_\-]{20,}$/.test(v)){
map.sessionid = v;
map.sessionid_ss = v;
}
}
function scanForSession(obj){
if(!obj || map.sessionid) return;
if(typeof obj === 'string'){
var m = obj.match(/(?:sessionid|sessionid_ss)["'\s:=]+([A-Za-z0-9_\-]{20,})/i) || obj.match(/sid_guard["'\s:=]+([^|"';&\s]+)\|/i);
if(m && m[1]) saveSessionId(m[1]);
return;
}
if(typeof obj !== 'object') return;
for(var k in obj){
if(!Object.prototype.hasOwnProperty.call(obj,k)) continue;
var lk = String(k).toLowerCase();
if(lk === 'sessionid' || lk === 'sessionid_ss' || lk === 'sid_guard' || lk === 'sid_tt') saveSessionId(obj[k]);
else scanForSession(obj[k]);
if(map.sessionid) return;
}
}
function pack(){
deriveSessionId();
return Object.keys(map).map(function(k){ return k+'='+map[k]; }).join('; ');
}
// 1) cookieStore 优先:可读 httpOnly cookieChrome 87+
function tryCookieStore(){
if(!window.cookieStore) return Promise.resolve(false);
return cookieStore.getAll().then(function(list){
if(!list||!list.length) return false;
list.forEach(function(c){ if(c&&c.name) map[c.name]=c.value; });
return true;
}).catch(function(){ return false; });
}
// 2) fetch API 让服务端刷新 Set-Cookie,然后读取
function fetchAndRefresh(){
var jobs = [
'/aweme/v1/web/query/user/',
'/aweme/v1/creator/im/user_detail/?aid=2906&app_name=aweme_creator_platform&device_platform=web&referer=https%3A%2F%2Fwww.douyin.com%2F'
];
return jobs.reduce(function(p,url){
return p.then(function(){
return fetch(url,{credentials:'include'})
.then(function(r){ return r.text(); })
.then(function(t){
try{
var d=JSON.parse(t);
scanForSession(d);
if(d&&(d.sessionid||d.cookie)) merge(d.sessionid||d.cookie);
if(d&&d.user_uid) window.__dyCollectedUid=String(d.user_uid);
}catch(e){ scanForSession(t); }
merge(document.cookie);
}).catch(function(){ merge(document.cookie); });
});
}, Promise.resolve());
}
// 3) iframe 刷新:强制浏览器请求 douyin.com 触发 Set-Cookie
function iframeRefresh(){
try{
var ifr=document.createElement('iframe');
ifr.style='display:none;width:0;height:0;';
ifr.src='/aweme/v1/web/query/user/';
document.body.appendChild(ifr);
return new Promise(function(resolve){
setTimeout(function(){ try{ifr.remove();}catch(e){} resolve(); },1200);
});
}catch(e){ return Promise.resolve(); }
}
merge(document.cookie);
return fetchAndRefresh().then(function(){
return tryCookieStore().then(function(csOk){
if(!csOk){
// cookieStore 不可用,用 fetch 后的 document.cookie
merge(document.cookie);
}
// 再试一次 cookieStorefetch 后可能 Set-Cookie 已更新)
return tryCookieStore();
});
}).then(function(){
var result=pack();
// sessionid 是 httpOnlydocument.cookie 和 cookieStore 可能都拿不到
if(result.indexOf('sessionid=')<0){
// 最后手段:iframe 刷新
return iframeRefresh().then(function(){
merge(document.cookie);
return tryCookieStore();
}).then(function(){ return pack(); });
}
return result;
}).catch(function(){ return pack(); });
}
function fetchUid(){
var local = __dyExtractUidLocal(o.localStorage, o.cookie_header || o.cookie);
if(local) return Promise.resolve(local);
if(window.__dyCollectedUid) return Promise.resolve(window.__dyCollectedUid);
var urls = [
'https://www.douyin.com/aweme/v1/web/query/user/?aid=6383&device_platform=webapp&channel=channel_pc_web',
'https://creator.douyin.com/aweme/v1/creator/user/info/?aid=6383&device_platform=webapp',
'https://www.douyin.com/aweme/v1/web/user/profile/self/?aid=6383&device_platform=webapp&channel=channel_pc_web',
'/aweme/v1/web/query/user/?aid=6383&device_platform=webapp&channel=channel_pc_web',
'/aweme/v1/creator/user/info/?aid=6383&device_platform=webapp'
];
function tryUrl(i){
if(i >= urls.length){
return Promise.resolve(__dyExtractUidLocal(o.localStorage, o.cookie_header || o.cookie));
}
return fetch(urls[i], {credentials:'include'})
.then(function(r){ return r.json(); })
.then(function(d){
var uid = __dyPickUid(d);
return uid || tryUrl(i + 1);
})
.catch(function(){ return tryUrl(i + 1); });
}
return tryUrl(0);
}
function showPanel(s, tip){
var old = document.getElementById('dy_collect_panel');
if (old) old.remove();
var div = document.createElement('div');
div.id = 'dy_collect_panel';
div.style = "position:fixed;top:8%;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:92%;max-width:620px;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 10px;font-size:13px;color:#9aa0c0;'>" + tip + "</p>" +
"<div id='dy_sid_box' style='margin:10px 0;padding:10px;border:1px solid #3b2f6b;border-radius:8px;background:#15162a;'>" +
"<div style='font-size:13px;color:#cbd5e1;margin-bottom:6px;'>如果仍缺 sessionid:在这里粘贴 <b>sessionid 值</b> 或 <b>完整 Cookie</b>,会自动合并到 JSON。</div>" +
"<input id='dy_sid_input' style='width:100%;height:34px;background:#0c0d1a;color:#e6e6f0;border:1px solid #2c2e4a;border-radius:8px;padding:0 8px;font-size:12px;outline:none;' placeholder='sessionid=xxxx; sid_guard=... 或只粘贴 sessionid 的值' />" +
"<button id='dy_btn_merge_sid' style='margin-top:8px;background:#34d399;color:#062014;border:none;border-radius:8px;padding:7px 12px;cursor:pointer;font-weight:700;font-size:12px;'>合并 sessionid 并复制 JSON</button>" +
"</div>" +
"<textarea id='dy_copy_area' style='width:100%;height:120px;background:#0c0d1a;color:#cbd5e1;border:1px solid #2c2e4a;border-radius:8px;padding:8px;font-family:monospace;font-size:11px;outline:none;resize:none;' readonly></textarea>" +
"<div style='margin-top:12px;display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap;'>" +
"<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;'>复制 JSON</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 copyArea = document.getElementById('dy_copy_area');
copyArea.value = s;
function parseCookieLine(raw){
var out={};
raw=String(raw||'').trim().replace(/^cookie\s*:/i,'');
raw.split(';').forEach(function(pair){
var i=pair.indexOf('=');
if(i>0) out[pair.slice(0,i).trim()] = pair.slice(i+1).trim();
});
return out;
}
function copyNow(text, btn){
copyArea.value = text;
copyArea.select();
try { navigator.clipboard.writeText(text); } catch(e) { document.execCommand('copy'); }
if(btn){ btn.innerText = '已复制!'; setTimeout(function(){ btn.innerText = btn.getAttribute('data-text') || '复制 JSON'; }, 1200); }
}
document.getElementById('dy_btn_merge_sid').onclick = function(){
var raw = document.getElementById('dy_sid_input').value.trim();
if(!raw){ this.innerText='请先粘贴'; return; }
var btn=this; btn.setAttribute('data-text','合并 sessionid 并复制 JSON');
try{
var obj=JSON.parse(copyArea.value || s);
var header=obj.cookie_header || obj.cookie || '';
var ck=parseCookieLine(raw);
var sid=ck.sessionid || ck.sessionid_ss || '';
if(!sid && ck.sid_guard){ try{sid=decodeURIComponent(ck.sid_guard).split('|')[0];}catch(e){sid=ck.sid_guard.split('|')[0];} }
if(!sid && raw.indexOf('=')<0) sid=raw;
if(!sid){ btn.innerText='没识别到 sessionid'; return; }
var merged=parseCookieLine(header);
for(var k in ck) merged[k]=ck[k];
merged.sessionid=sid;
merged.sessionid_ss=sid;
obj.cookie_header=Object.keys(merged).map(function(k){return k+'='+merged[k];}).join('; ');
obj.cookie=obj.cookie_header;
copyNow(JSON.stringify(obj), btn);
}catch(e){ btn.innerText='JSON 合并失败'; }
};
document.getElementById('dy_btn_copy').onclick = function(){
this.setAttribute('data-text','复制 JSON');
copyNow(copyArea.value || s, this);
};
document.getElementById('dy_btn_close').onclick = function(){ div.remove(); };
}
function finish(){
refreshWs();
if(!o.my_uid) o.my_uid = __dyExtractUidLocal(o.localStorage, o.cookie_header || o.cookie);
if(!o.my_uid && o.frontier_ws_url) o.my_uid = __dyUidFromWs(o.frontier_ws_url);
var header = o.cookie_header || o.cookie || '';
o.cookie_header = header;
o.cookie = header;
var s = JSON.stringify(o);
var tips = [];
if (header.indexOf('sessionid=') >= 0) {
tips.push('✓ Cookie(含 sessionid');
} else {
tips.push('⚠ 缺 sessionidhttpOnly,页面脚本无法直接读取):');
tips.push(' ① F12 → Network → 任意 creator.douyin.com 请求 → Request Headers → Cookie 整段');
tips.push(' ② 或右键请求 → Copy → Copy as fetch,把整段 fetch 粘到采集工具「完整 Cookie 头」框');
tips.push(' ③ 工具会自动从 fetch 里的 headers.cookie 提取 sessionid');
}
if (o.localStorage['security-sdk/s_sdk_sign_data_key/web_protect']) tips.push('✓ localStorage 签名');
else tips.push('⚠ 缺 web_protect:打开私信页停留 5 秒后再点书签');
if (o.frontier_ws_url) tips.push('✓ WebSocket' + (o.frontier_ws_url.indexOf('sdk_cert=')>0?'含 sdk_cert':'基础连接') + '');
else {
tips.push('⚠ 未捕获 WS');
tips.push(' ① 点书签安装监听 → 刷新私信页(等看到消息列表)→ 再点一次书签');
tips.push(' ② 或在私信页 F12 → Network → WS 标签 → 复制 frontier-im 的 URL');
}
if (o.my_uid) tips.push('✓ UID ' + o.my_uid);
var tip = tips.join('<br/>') + '<br/><br/>已复制到剪贴板,请回到采集工具粘贴。';
try { navigator.clipboard.writeText(s); } catch(e) {
try { copy(s); } catch(e2){}
}
showPanel(s, tip);
console.log('[抖音一键采集]', o);
}
var fetchedUid = false, fetchedCookie = false, elapsed = 0, maxWait = 8000, interval = 300;
function tryFinish(){
refreshWs();
if(!o.my_uid) o.my_uid = __dyExtractUidLocal(o.localStorage, o.cookie_header || o.cookie);
if(!o.my_uid && o.frontier_ws_url) o.my_uid = __dyUidFromWs(o.frontier_ws_url);
var ready = fetchedUid && fetchedCookie && (o.frontier_ws_url || elapsed >= 5000);
if (ready || elapsed >= maxWait) finish();
else { elapsed += interval; setTimeout(tryFinish, interval); }
}
collectCookies().then(function(header){
o.cookie_header = header;
o.cookie = header;
fetchedCookie = true;
}).catch(function(){ fetchedCookie = true; });
fetchUid().then(function(uid){ if(uid) o.my_uid = uid; fetchedUid = true; })
.catch(function(){ fetchedUid = true; });
setTimeout(tryFinish, 0);
})()`;
function compressBookmarkletCode(code) {
// 必须先删 // 注释再压空白;用 [^:] 避免误伤 https://
return code.replace(/(^|[^:])\/\/[^\n\r]*/g, '$1').replace(/\s+/g, ' ').trim();
}
const SNIPPET_CODE = COLLECTOR_CODE.replace(/navigator\.clipboard\.writeText\(s\)/g, '(function(){try{copy(s);}catch(e){}})()');
const SNIPPET = SNIPPET_CODE;
document.getElementById('snippet').innerText = SNIPPET;
const BOOKMARKLET = "javascript:" + encodeURIComponent(compressBookmarkletCode(COLLECTOR_CODE));
document.getElementById('bookmarklet').setAttribute('href', BOOKMARKLET);
function toast(msg) {
const t = document.getElementById('toast');
t.innerText = msg; t.classList.add('show');
clearTimeout(t._t); t._t = setTimeout(() => t.classList.remove('show'), 1600);
}
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 || '已复制');
});
}
// Playwright storage_state(抖音一键采集器.py 输出)→ 工具内部格式
function isStorageStateFormat(obj) {
return obj && Array.isArray(obj.cookies) && Array.isArray(obj.origins);
}
function lsEntriesToMap(entries) {
const ls = {};
(entries || []).forEach(e => {
if (e && e.name) ls[e.name] = e.value;
});
return ls;
}
function storageStateToDump(obj) {
const cookie = (obj.cookies || [])
.filter(c => c && c.name && c.value != null)
.map(c => c.name + '=' + c.value)
.join('; ');
const ls = {};
(obj.origins || []).forEach(o => Object.assign(ls, lsEntriesToMap(o.localStorage)));
const ws = obj.frontier_ws_url || '';
const wsDev = uidFromWsUrl(ws);
const myUid = (obj.my_uid != null ? String(obj.my_uid) : '')
|| (ws.includes('aid=2906') && wsDev ? wsDev : '')
|| extractMyUid(ls);
return {
ls,
cookie: deriveSessionCookie(cookie),
my_uid: myUid,
ws,
ws_urls: ws ? [ws] : [],
_storageState: true,
_raw: obj,
};
}
// 把采集到的原始 JSON 规整为 {ls, cookie, my_uid, ws, ws_urls}
function normalizeDump(raw) {
const obj = parseLocalStorage(raw);
if (obj === null) return null;
if (isStorageStateFormat(obj)) return storageStateToDump(obj);
let cookie = mergeCookieHeaders(obj.cookie_header || '', obj.cookie || '');
cookie = deriveSessionCookie(cookie + '; ' + extractEmbeddedCookieFromRaw(raw));
const my_uid = obj.my_uid || '';
const ws_urls = Array.isArray(obj.ws_urls) ? obj.ws_urls : [];
const ws = obj.frontier_ws_url || ws_urls[0] || '';
const base = { cookie, my_uid, ws, ws_urls };
if (obj.localStorage && typeof obj.localStorage === 'object' && !Array.isArray(obj.localStorage)) {
return { ls: obj.localStorage, ...base };
}
return { ls: obj, ...base };
}
function isImWsUrl(u) {
u = String(u || '');
if (!u || !/^wss?:/i.test(u) || !/[?&]token=/.test(u)) return false;
if (/frontier/i.test(u)) return true;
if (/zijieapi\.com/i.test(u)) return true;
return /\/ws\/v2/i.test(u) && /douyin/i.test(u);
}
function uidFromWsUrl(ws) {
if (!ws) return '';
const m = String(ws).match(/[?&]device_id=(\d{6,})/);
return m ? m[1] : '';
}
function wsDeviceMatchesUid(ws, uid) {
if (!ws || !uid) return true;
const wsDev = uidFromWsUrl(ws);
return !wsDev || String(wsDev) === String(uid);
}
// IM 数字 UID = tea_cache_tokens.user_unique_id
function extractMyUid(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 || '';
if (id && /^\d+$/.test(String(id))) return String(id);
} catch (e) {}
}
return '';
}
function collectLsFromOrigins(origins) {
const ls = {};
(origins || []).forEach(o => Object.assign(ls, lsEntriesToMap(o.localStorage)));
return ls;
}
function mergeSupplementIntoCookies(cookies, supplement, sessionidQuick) {
const map = {};
(cookies || []).forEach(c => { if (c && c.name) map[c.name] = { ...c }; });
const farFuture = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 180;
function addFromHeader(str) {
deriveSessionCookie(str).split(/;\s*/).forEach(pair => {
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;
const prev = map[name];
map[name] = {
name, value,
domain: prev?.domain || '.douyin.com',
path: prev?.path || '/',
expires: prev?.expires || farFuture,
httpOnly: prev?.httpOnly ?? ['sessionid', 'sessionid_ss'].includes(name),
secure: prev?.secure !== false,
sameSite: prev?.sameSite || 'None',
};
});
}
if (supplement) addFromHeader(supplement);
if (sessionidQuick) {
let sid = sessionidQuick.trim();
try { sid = decodeURIComponent(sid); } catch (e) {}
sid = sid.split('|')[0].trim();
['sessionid', 'sessionid_ss'].forEach(name => {
map[name] = {
name, value: sid,
domain: '.douyin.com', path: '/',
expires: farFuture, httpOnly: true, secure: true, sameSite: 'None',
};
});
}
return Object.values(map);
}
function normalizeStorageStateForIm(output) {
const ls = collectLsFromOrigins(output.origins);
const uidInput = document.getElementById('uidInput').value.trim();
let ws = (document.getElementById('wsInput').value || output.frontier_ws_url || '').trim();
if (ws) ws = extractWsUrl(ws) || (isImWsUrl(ws) ? ws : '');
const wsDev = uidFromWsUrl(ws);
const isCreatorWs = ws && ws.includes('aid=2906') && !!(qparam(ws, 'sdk_cert') || output.sdk_cert);
let myUid = (isCreatorWs && wsDev) ? wsDev
: ((uidInput && /^\d+$/.test(uidInput)) ? uidInput : (extractMyUid(ls) || extractWebId(ls)));
if (myUid) {
output.my_uid = parseInt(myUid, 10);
document.getElementById('uidInput').value = String(myUid);
for (const origin of (output.origins || [])) {
const arr = origin.localStorage || [];
const dev = pickLs(collectLsFromOrigins([origin]), 'web_runtime_security_uid', ['runtime_security_uid']);
if (!dev || !/^\d+$/.test(String(dev))) {
const idx = arr.findIndex(e => e.name === 'web_runtime_security_uid');
const entry = { name: 'web_runtime_security_uid', value: String(myUid) };
if (idx >= 0) arr[idx] = entry; else arr.push(entry);
}
}
}
if (ws) {
if (ws && myUid && !wsDeviceMatchesUid(ws, myUid) && !isCreatorWs) {
delete output.frontier_ws_url;
delete output.sdk_cert;
delete output.ts_sign;
document.getElementById('wsInput').value = '';
ws = '';
} else if (ws) {
output.frontier_ws_url = ws;
const cert = qparam(ws, 'sdk_cert');
const ts = qparam(ws, 'ts_sign');
if (cert) output.sdk_cert = cert;
if (ts) output.ts_sign = ts;
document.getElementById('wsInput').value = ws;
}
}
return output;
}
function buildChecksFromState(storageState, ls) {
const cookies = storageState.cookies || [];
const names = new Set(cookies.map(c => (c.name || '').toLowerCase()));
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 devRaw = pickLs(ls, 'web_runtime_security_uid', ['runtime_security_uid', 'security_uid']);
const deviceId = (/^\d+$/.test(String(devRaw || '')) ? devRaw : '') || extractMyUid(ls) || webId;
const finalUid = storageState.my_uid ? String(storageState.my_uid) : extractMyUid(ls);
const wsRaw = storageState.frontier_ws_url || '';
const wp = parseWebProtect(valProtect);
const checks = [];
const hasSession = names.has('sessionid') || names.has('sessionid_ss');
const hasUid = !!finalUid;
const hasProtect = !!valProtect;
const hasKeys = !!valKeys;
const hasWebId = !!webId || !!extractMyUid(ls);
const hasDevice = !!deviceId && /^\d+$/.test(String(deviceId));
const hasWpTicket = !!(wp && wp.ticket);
const hasWpTs = !!(wp && wp.ts_sign);
const hasWpCert = !!(wp && (wp.client_cert || wp.sdk_cert));
let hasWs = false, wsSdkCert = storageState.sdk_cert || '';
checks.push(chk(hasSession, 'sessionid(登录态,必需)', '缺 sessionid!请在上方「Cookie 补充」粘贴 Network 请求头里的 Cookie 整行(含 sessionid='));
checks.push(chk(hasUid, '抖音数字 UIDuser_unique_id', '未提取到数字 UID:请重新采集或手动填写 tea_cache_tokens 里的 user_unique_id'));
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:已尝试用 UID 兜底;若 IM 仍失败请重新采集'));
if (wsRaw) {
const isCreatorWs = wsRaw.includes('aid=2906') && !!(storageState.sdk_cert || qparam(wsRaw, 'sdk_cert'));
const uidForWs = isCreatorWs ? (uidFromWsUrl(wsRaw) || finalUid) : finalUid;
const wsOk = wsDeviceMatchesUid(wsRaw, uidForWs);
if (wsOk && /[?&]token=/.test(wsRaw)) {
hasWs = true;
wsSdkCert = wsSdkCert || qparam(wsRaw, 'sdk_cert');
if (wsSdkCert && isCreatorWs) checks.push({ s: 'ok', t: '已包含创作者私信 WSaid=2906,含 sdk_cert' });
else if (wsSdkCert) checks.push({ s: 'ok', t: '已包含 frontier WebSocket(含 sdk_cert,用于实时收消息)' });
else checks.push({ s: 'warn', t: '已包含 frontier WebSocket,但未解析到 sdk_cert;请在创作者私信页刷新后重新采集' });
} else if (!wsOk) {
checks.push({ s: 'warn', t: '已丢弃 device_id 与 UID 不一致的 WS(旧连接);导入后系统会自动重建 WS' });
}
} else if (hasSession) {
checks.push({ s: 'warn', t: '未抓到 frontier WS,但已有 sessionid:导入后系统会自动推导 WS 连接' });
} else {
checks.push({ s: 'warn', t: '未获取 frontier WS:补全 sessionid 后,在私信页点书签 → 刷新 → 再点一次' });
}
const okCore = hasSession && hasProtect && hasKeys && hasWpTicket && hasWpTs && hasWpCert && hasUid;
return { checks, okCore, hasWebId, hasDevice };
}
function generateFromStorageState(rawObj) {
let output = JSON.parse(JSON.stringify(rawObj));
const supplement = document.getElementById('cookieSupplement').value.trim();
const sessionidQuick = document.getElementById('sessionidInput').value.trim();
if (supplement || sessionidQuick) {
output.cookies = mergeSupplementIntoCookies(output.cookies, supplement, sessionidQuick);
}
output = normalizeStorageStateForIm(output);
if (rawObj.user_agent) output.user_agent = rawObj.user_agent;
document.getElementById('output').value = JSON.stringify(output, null, 2);
const ls = collectLsFromOrigins(output.origins);
const { checks, okCore, hasWebId, hasDevice } = buildChecksFromState(output, ls);
renderChecks(checks);
toast(okCore ? (hasWebId && hasDevice ? '生成成功,凭证齐全,可复制导入' : '核心凭证齐全可用;建议核对 UID/WS') : '关键凭证缺失,请看红色提示');
}
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 resolveWsFromDump(d, raw) {
const candidates = [];
if (d) {
if (d.ws) candidates.push(String(d.ws).trim());
if (Array.isArray(d.ws_urls)) d.ws_urls.forEach(u => { if (u) 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 mergeCookieHeaders(base, extra) {
const map = {};
function merge(str) {
(str || '').split(/;\s*/).forEach(pair => {
const idx = pair.indexOf('=');
if (idx <= 0) return;
map[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
});
}
merge(base);
merge(extractCookieFromRaw(extra).replace(/^cookie\s*:/i, ''));
return Object.keys(map).map(k => k + '=' + map[k]).join('; ');
}
function applyUnifiedDump(raw) {
const dump = normalizeDump(raw);
if (dump === null) {
toast('JSON 解析失败,请确认粘贴的是书签采集结果');
return false;
}
const cookieVal = dump.cookie || '';
if (cookieVal) document.getElementById('cookieInput').value = cookieVal;
document.getElementById('lsInput').value = raw.trim();
const ws = resolveWsFromDump(dump, raw);
if (ws) document.getElementById('wsInput').value = ws;
let uid = extractMyUid(dump.ls || {}) || dump.my_uid || '';
if (!uid && dump.ws && wsDeviceMatchesUid(dump.ws, extractMyUid(dump.ls || {}) || extractWebId(dump.ls || {}))) {
uid = uidFromWsUrl(dump.ws);
}
if (!uid && dump.ls) {
const webId = extractWebId(dump.ls);
if (webId) uid = webId;
}
if (uid) document.getElementById('uidInput').value = uid;
generate();
toast(dump._storageState ? '已识别一键采集器 JSON,凭证已填充' : '采集结果已填充');
return true;
}
let unifiedTimer = null;
document.getElementById('unifiedInput').addEventListener('input', function() {
clearTimeout(unifiedTimer);
const val = this.value.trim();
if (!val || val.length < 20) return;
unifiedTimer = setTimeout(function() { applyUnifiedDump(val); }, 400);
});
document.getElementById('unifiedInput').addEventListener('paste', function() {
clearTimeout(unifiedTimer);
setTimeout(function() {
const val = document.getElementById('unifiedInput').value.trim();
if (val) applyUnifiedDump(val);
}, 50);
});
let cookieSupTimer = null;
document.getElementById('cookieSupplement').addEventListener('input', function() {
clearTimeout(cookieSupTimer);
cookieSupTimer = setTimeout(function() {
const extra = document.getElementById('cookieSupplement').value.trim();
if (!extra) return;
const merged = mergeCookieHeaders(document.getElementById('cookieInput').value, extra);
document.getElementById('cookieInput').value = merged;
generate();
}, 400);
});
// 手动合并 sessionid
function applySessionId() {
const sid = document.getElementById('sessionidInput').value.trim();
if (!sid) { toast('请先粘贴 sessionid'); return; }
const cur = document.getElementById('cookieInput').value.trim();
// 去掉旧的 sessionid(如果有)
const cleaned = cur.replace(/sessionid=[^;]*;?\s*/g, '').replace(/sessionid_ss=[^;]*;?\s*/g, '');
const newCookie = 'sessionid=' + sid + '; sessionid_ss=' + sid + (cleaned ? '; ' + cleaned : '');
document.getElementById('cookieInput').value = newCookie;
generate();
toast('sessionid 已合并并重新生成');
}
document.getElementById('sessionidInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); applySessionId(); }
});
// 手动填入 WS URL
function applyWsUrl() {
const ws = document.getElementById('wsQuickInput').value.trim();
if (!ws) { toast('请先粘贴 WS URL'); return; }
document.getElementById('wsInput').value = ws;
generate();
toast('WS URL 已填入并重新生成');
}
document.getElementById('wsQuickInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); applyWsUrl(); }
});
// 从 localStorage 里智能提取某个字段:先精确键名,再模糊包含匹配
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 '';
}
// 从任意 localStorage 值里尽力挖出 web_iduser_unique_id
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) {}
}
// 兜底:扫描所有值,正则找 user_unique_id
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 '';
}
// 从 URL 取某个 query 参数并解码(decodeURIComponent 不会把 + 变空格,base64 安全)
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]; }
}
// 解析 web_protect JSON(兼容 {"data":"{...}"} 双层结构)
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;
}
}
// 监听粘贴/输入,自动填充 UID 框
document.getElementById('lsInput').addEventListener('input', () => {
const d = normalizeDump(document.getElementById('lsInput').value);
if (d && d.my_uid) {
document.getElementById('uidInput').value = d.my_uid;
}
});
function extractCookieFromRaw(raw) {
raw = (raw || "").trim();
const embedded = extractEmbeddedCookieFromRaw(raw);
if (embedded) return embedded;
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();
}
return raw;
}
function extractEmbeddedCookieFromRaw(raw) {
const text = String(raw || '');
const patterns = [
/\\"cookie\\"\s*:\s*\\"([\s\S]*?)\\"\s*,\s*\\"Referer\\"/i,
/"cookie"\s*:\s*"([\s\S]*?)"\s*,\s*"Referer"/i,
/\\"cookie\\"\s*:\s*\\"([\s\S]*?)\\"\s*[}\n]/i,
/"cookie"\s*:\s*"([\s\S]*?)"\s*[}\n]/i
];
for (const re of patterns) {
const m = text.match(re);
if (m && m[1] && /(?:sessionid|sid_guard|sid_tt)=/i.test(m[1])) {
return m[1].replace(/\\"/g, '"').replace(/\\u0026/g, '&').replace(/\\\//g, '/');
}
}
const idx = text.search(/(?:sessionid|sid_guard|sid_tt)=/i);
if (idx >= 0) {
let end = text.indexOf('"', idx);
if (end < 0) end = text.indexOf('\n', idx);
if (end < 0) end = text.length;
return text.slice(Math.max(0, idx - 600), end);
}
return '';
}
function deriveSessionCookie(raw) {
raw = String(raw || '');
const map = {};
raw.replace(/^cookie\s*:/i, '').split(/;\s*/).forEach(pair => {
const idx = pair.indexOf('=');
if (idx <= 0) return;
map[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
});
let sid = map.sessionid || map.sessionid_ss || '';
if (!sid) {
let guard = map.sid_guard || map.sid_tt || '';
if (guard) {
try { guard = decodeURIComponent(guard); } catch (e) {}
sid = String(guard).split('|')[0].trim();
}
}
if (!sid) {
const text = raw.replace(/\\u0026/g, '&').replace(/\\\//g, '/');
const m = text.match(/(?:sessionid|sessionid_ss)["'\s:=]+([A-Za-z0-9_\-]{20,})/i) ||
text.match(/sid_guard["'\s:=]+([^|"';&\s]+)\|/i);
if (m && m[1]) sid = m[1];
}
if (!sid) return raw;
try { sid = decodeURIComponent(sid); } catch (e) {}
sid = String(sid).split('|')[0].trim();
if (!/^[A-Za-z0-9_\-]{20,}$/.test(sid)) return raw;
map.sessionid = sid;
map.sessionid_ss = sid;
return Object.keys(map).length
? Object.keys(map).map(k => k + '=' + map[k]).join('; ')
: 'sessionid=' + sid + '; sessionid_ss=' + sid;
}
// 把 "a=b; c=d" 解析为 cookie 对象数组
function parseCookieHeader(raw) {
const out = [];
const farFuture = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 180; // 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();
// 过滤掉因为粘贴了 fetch 语句等导致解析出的非法的 Cookie 名
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 s = raw.indexOf('{'), e = raw.lastIndexOf('}');
if (s >= 0 && e > s) {
try { return JSON.parse(raw.slice(s, e + 1)); } catch (e2) {}
}
return null;
}
function generate() {
let cookieRaw = document.getElementById('cookieInput').value.trim();
const lsRaw = document.getElementById('lsInput').value.trim();
let myUidVal = document.getElementById('uidInput').value.trim();
const checks = [];
const rawObj = lsRaw ? parseLocalStorage(lsRaw) : null;
if (rawObj && isStorageStateFormat(rawObj)) {
if (!cookieRaw && rawObj.cookies?.length) {
document.getElementById('cookieInput').value = rawObj.cookies.map(c => c.name + '=' + c.value).join('; ');
}
generateFromStorageState(rawObj);
return;
}
const dump = lsRaw ? normalizeDump(lsRaw) : null;
if (dump === null && lsRaw) {
renderChecks([{ s:'err', t:'步骤 3 的 JSON 解析失败,请重新复制采集结果' }]);
return;
}
const ls = dump ? (dump.ls || {}) : {};
if (!cookieRaw && dump && dump.cookie) {
cookieRaw = dump.cookie;
document.getElementById('cookieInput').value = cookieRaw;
}
if (!myUidVal && dump && dump.my_uid) {
myUidVal = dump.my_uid;
document.getElementById('uidInput').value = myUidVal;
}
if (!myUidVal && dump && dump.ls) {
const fallbackUid = extractMyUid(dump.ls) || extractWebId(dump.ls);
if (fallbackUid) {
myUidVal = fallbackUid;
document.getElementById('uidInput').value = myUidVal;
}
}
if (!cookieRaw) {
renderChecks([{ s:'err', t:'请先一键采集或粘贴完整 Cookie(步骤 2)' }]);
return;
}
const supplement = document.getElementById('cookieSupplement').value.trim();
if (supplement) {
cookieRaw = mergeCookieHeaders(cookieRaw, supplement);
document.getElementById('cookieInput').value = cookieRaw;
}
let cleanCookie = extractCookieFromRaw(cookieRaw).replace(/^cookie\s*:/i, '').trim();
cleanCookie = deriveSessionCookie(cleanCookie);
document.getElementById('cookieInput').value = cleanCookie;
const cookies = parseCookieHeader(cleanCookie);
const names = new Set(cookies.map(c => c.name.toLowerCase()));
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 myUidFromTea = extractMyUid(ls);
const devRaw = pickLs(ls, 'web_runtime_security_uid', ['runtime_security_uid', 'security_uid']);
const deviceId = (/^\d+$/.test(String(devRaw || '')) ? devRaw : '') || myUidFromTea || 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() || myUidFromTea || (dump && dump.my_uid) || '';
if (finalUid) storageState.my_uid = parseInt(finalUid, 10);
let wsRaw = (document.getElementById('wsInput').value || '').trim();
if (!wsRaw && dump) wsRaw = resolveWsFromDump(dump, lsRaw);
if (wsRaw) document.getElementById('wsInput').value = wsRaw;
if (!myUidVal && wsRaw && wsDeviceMatchesUid(wsRaw, myUidFromTea || webId)) {
const uidFromWs = uidFromWsUrl(wsRaw);
if (uidFromWs) {
myUidVal = uidFromWs;
document.getElementById('uidInput').value = myUidVal;
}
}
let hasWs = false, wsSdkCert = '', wsTsSign = '';
if (wsRaw) {
const wsUrl = extractWsUrl(wsRaw) || (isImWsUrl(wsRaw) ? wsRaw : '');
const uidForWs = finalUid || myUidVal || myUidFromTea;
const isFrontier = wsUrl && /[?&]token=/.test(wsUrl) &&
(wsUrl.indexOf('frontier-im.douyin.com') !== -1 || (wsUrl.indexOf('frontier') !== -1 && wsUrl.indexOf('zijieapi.com') !== -1)) &&
wsDeviceMatchesUid(wsUrl, uidForWs);
if (isFrontier) {
hasWs = true;
storageState.frontier_ws_url = wsUrl;
wsSdkCert = qparam(wsUrl, 'sdk_cert');
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 { checks: builtChecks, okCore, hasWebId: okWebId, hasDevice: okDevice } = buildChecksFromState(storageState, ls);
renderChecks(builtChecks);
toast(okCore ? (okWebId && okDevice ? '生成成功,凭证齐全,可复制导入' : '核心凭证齐全可用;web_id/device_id 建议补全') : '关键凭证缺失,请看红色提示');
}
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('');
}
</script>
</body>
</html>