1342 lines
56 KiB
HTML
1342 lines
56 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>抖音凭证采集工具 · 本地生成 storage_state</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0f1020;
|
||
--panel: #1a1b2e;
|
||
--panel2: #23244044;
|
||
--border: #2c2e4a;
|
||
--text: #e6e6f0;
|
||
--muted: #9aa0c0;
|
||
--accent: #8b5cf6;
|
||
--accent2: #a855f7;
|
||
--ok: #34d399;
|
||
--warn: #fbbf24;
|
||
--err: #f87171;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
background: radial-gradient(1200px 600px at 70% -10%, #2a1c4a55, transparent), var(--bg);
|
||
color: var(--text);
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
|
||
line-height: 1.6;
|
||
padding: 32px 16px 80px;
|
||
}
|
||
.wrap { max-width: 920px; margin: 0 auto; }
|
||
h1 { font-size: 24px; margin: 0 0 4px; }
|
||
.sub { color: var(--muted); margin: 0 0 24px; font-size: 14px; }
|
||
.card {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
padding: 20px 22px;
|
||
margin-bottom: 18px;
|
||
}
|
||
.step-h {
|
||
display: flex; align-items: center; gap: 10px;
|
||
font-weight: 600; font-size: 16px; margin-bottom: 10px;
|
||
}
|
||
.badge {
|
||
width: 26px; height: 26px; flex: none;
|
||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||
border-radius: 50%; display: grid; place-items: center;
|
||
font-size: 14px; font-weight: 700; color: #fff;
|
||
}
|
||
p.tip { color: var(--muted); font-size: 13.5px; margin: 6px 0; }
|
||
code.k { background: #00000055; padding: 1px 6px; border-radius: 5px; color: #c4b5fd; font-size: 13px; }
|
||
textarea {
|
||
width: 100%; min-height: 92px; resize: vertical;
|
||
background: #0c0d1a; color: var(--text);
|
||
border: 1px solid var(--border); border-radius: 10px;
|
||
padding: 12px; font-family: ui-monospace, Consolas, monospace; font-size: 12.5px;
|
||
outline: none;
|
||
}
|
||
textarea:focus { border-color: var(--accent); }
|
||
.snippet {
|
||
position: relative; background: #0c0d1a; border: 1px solid var(--border);
|
||
border-radius: 10px; padding: 12px 12px; margin: 10px 0;
|
||
font-family: ui-monospace, Consolas, monospace; font-size: 12.5px;
|
||
white-space: pre-wrap; word-break: break-all; color: #a5b4fc;
|
||
}
|
||
button {
|
||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||
color: #fff; border: none; border-radius: 10px;
|
||
padding: 10px 18px; font-size: 14px; font-weight: 600; cursor: pointer;
|
||
transition: opacity .15s;
|
||
}
|
||
button:hover { opacity: .9; }
|
||
button.ghost {
|
||
background: transparent; border: 1px solid var(--border); color: var(--muted);
|
||
padding: 6px 12px; font-size: 12.5px; font-weight: 500;
|
||
}
|
||
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 12px; }
|
||
.out { min-height: 160px; }
|
||
.checks { margin-top: 12px; display: grid; gap: 6px; }
|
||
.chk { display: flex; align-items: center; gap: 8px; font-size: 13.5px; }
|
||
.dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
|
||
.ok .dot { background: var(--ok); } .ok { color: var(--ok); }
|
||
.warn .dot { background: var(--warn); } .warn { color: var(--warn); }
|
||
.err .dot { background: var(--err); } .err { color: var(--err); }
|
||
.hint-box {
|
||
background: #1e1b3a; border: 1px solid #3b2f6b; border-radius: 10px;
|
||
padding: 10px 14px; margin: 10px 0; font-size: 13px; color: #cbd5e1;
|
||
}
|
||
ol { margin: 6px 0 6px 4px; padding-left: 20px; }
|
||
ol li { margin: 4px 0; font-size: 13.5px; }
|
||
.pill { font-size: 11px; color: var(--muted); border: 1px solid var(--border); padding: 1px 8px; border-radius: 999px; }
|
||
.toast {
|
||
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
||
background: #111; border: 1px solid var(--accent); color: #fff;
|
||
padding: 10px 18px; border-radius: 10px; opacity: 0; transition: opacity .2s; pointer-events: none;
|
||
font-size: 13.5px;
|
||
}
|
||
.toast.show { opacity: 1; }
|
||
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 cookie(Chrome 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);
|
||
}
|
||
// 再试一次 cookieStore(fetch 后可能 Set-Cookie 已更新)
|
||
return tryCookieStore();
|
||
});
|
||
}).then(function(){
|
||
var result=pack();
|
||
// sessionid 是 httpOnly,document.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('⚠ 缺 sessionid(httpOnly,页面脚本无法直接读取):');
|
||
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, '抖音数字 UID(user_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_id:localStorage 里没找到 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: '已包含创作者私信 WS(aid=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_id(user_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>
|