Files

507 lines
18 KiB
JavaScript

// ==UserScript==
// @name Pandy-ComfyUI Token Grabber
// @namespace http://tampermonkey.net/
// @version 3.5
// @description 直接从 ComfyUI 提取 auth_token_comfy_org 并同步到 Pandy
// @author You
// @match *://localhost:8188/*
// @match *://127.0.0.1:8188/*
// @match *://127.0.0.1:8180/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect 127.0.0.1
// @connect localhost
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'comfyui_auth_token';
const POS_KEY = 'token_grabber_pos';
const COLLAPSED_KEY = 'token_grabber_collapsed';
let foundToken = '';
let isCollapsed = GM_getValue(COLLAPSED_KEY, false);
let savedPos = GM_getValue(POS_KEY, { x: null, y: 10 });
// 确保位置在可视范围内
function clampPosition() {
const panel = document.getElementById('token-grabber-panel');
if (!panel) return;
const rect = panel.getBoundingClientRect();
let needUpdate = false;
// 如果面板超出右边界
if (rect.right > window.innerWidth) {
panel.style.left = 'auto';
panel.style.right = '10px';
needUpdate = true;
}
// 如果面板超出下边界
if (rect.bottom > window.innerHeight) {
panel.style.top = Math.max(10, window.innerHeight - rect.height - 10) + 'px';
needUpdate = true;
}
// 如果面板超出左边界
if (rect.left < 0) {
panel.style.left = '10px';
panel.style.right = 'auto';
needUpdate = true;
}
// 如果面板超出上边界
if (rect.top < 0) {
panel.style.top = '10px';
needUpdate = true;
}
}
// 搜索 token
function searchToken() {
// 1. localStorage
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
if (value && value.includes('eyJ')) {
const token = extractJWT(value);
if (token && isValidJWT(token)) return token;
}
}
// 2. sessionStorage
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
const value = sessionStorage.getItem(key);
if (value && value.includes('eyJ')) {
const token = extractJWT(value);
if (token && isValidJWT(token)) return token;
}
}
// 3. window 对象
const props = ['app', 'api', 'comfyAPI'];
for (const prop of props) {
if (window[prop]) {
const token = deepSearch(window[prop], 'auth_token_comfy_org', 5);
if (token) return token;
}
}
return null;
}
function extractJWT(str) {
const match = str.match(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/);
return match ? match[0] : null;
}
function isValidJWT(token) {
try {
const parts = token.split('.');
if (parts.length !== 3) return false;
let payload = parts[1];
payload += '='.repeat((4 - payload.length % 4) % 4);
const info = JSON.parse(atob(payload));
return info.exp !== undefined;
} catch (e) { return false; }
}
function deepSearch(obj, key, depth) {
if (depth <= 0 || !obj || typeof obj !== 'object') return null;
try {
if (obj[key]) return obj[key];
for (const k in obj) {
const result = deepSearch(obj[k], key, depth - 1);
if (result) return result;
}
} catch (e) { }
return null;
}
function parseToken(token) {
try {
const parts = token.split('.');
let payload = parts[1];
payload += '='.repeat((4 - payload.length % 4) % 4);
const info = JSON.parse(atob(payload));
const remaining = Math.floor((info.exp * 1000 - Date.now()) / 60000);
return { valid: remaining > 0, remaining };
} catch (e) { return { valid: false, remaining: 0 }; }
}
function updatePanel() {
const statusEl = document.getElementById('tg-status');
const previewEl = document.getElementById('tg-preview');
const expiryEl = document.getElementById('tg-expiry');
const iconStatus = document.getElementById('tg-icon-status');
if (!foundToken) {
if (statusEl) {
statusEl.textContent = '❌ 未找到';
statusEl.className = 'status expired';
}
if (previewEl) previewEl.textContent = '请确保已登录';
if (iconStatus) iconStatus.style.background = '#f66';
return;
}
const info = parseToken(foundToken);
if (info.valid) {
if (statusEl) {
statusEl.textContent = '✅ Token 已获取';
statusEl.className = 'status captured';
}
if (expiryEl) expiryEl.innerHTML = `⏱️ 剩余 <b>${info.remaining}</b> 分钟`;
if (iconStatus) iconStatus.style.background = '#6f6';
} else {
if (statusEl) {
statusEl.textContent = '❌ Token 已过期';
statusEl.className = 'status expired';
}
if (expiryEl) expiryEl.innerHTML = '请重新登录';
if (iconStatus) iconStatus.style.background = '#f66';
}
if (previewEl) previewEl.textContent = foundToken.substring(0, 50) + '...';
}
function copyToken() {
if (foundToken) {
GM_setClipboard(foundToken);
const btn = document.getElementById('tg-copy-btn');
if (btn) {
btn.textContent = '✅ 已复制!';
setTimeout(() => btn.textContent = '📋 复制 Token', 1500);
}
} else {
alert('未找到 Token');
}
}
function refreshToken() {
foundToken = searchToken() || '';
if (foundToken) GM_setValue(STORAGE_KEY, foundToken);
updatePanel();
}
function toggleCollapse() {
isCollapsed = !isCollapsed;
GM_setValue(COLLAPSED_KEY, isCollapsed);
const panel = document.getElementById('token-grabber-panel');
if (isCollapsed) {
panel.classList.add('collapsed');
} else {
panel.classList.remove('collapsed');
}
}
// 同步到 Pandy Image 页面(使用 GM_xmlhttpRequest 绕过跨域)
async function syncToPandy() {
if (!foundToken) {
alert('未找到 Token');
return;
}
const btn = document.getElementById('tg-sync-btn');
if (btn) btn.textContent = '⏳ 同步中...';
const PANDY_BASE = 'http://127.0.0.1:8180';
// 使用 GM_xmlhttpRequest 绕过跨域限制
// 1. 先获取现有配置
GM_xmlhttpRequest({
method: 'GET',
url: `${PANDY_BASE}/config?name=settings`,
onload: function (response) {
let config = {};
try {
if (response.status === 200) {
config = JSON.parse(response.responseText);
}
} catch (e) { }
// 2. 合并 token
config.token = foundToken;
// 3. POST 回去
GM_xmlhttpRequest({
method: 'POST',
url: `${PANDY_BASE}/config?name=settings`,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(config),
onload: function (resp) {
if (resp.status === 200) {
if (btn) {
btn.textContent = '✅ 已同步!';
setTimeout(() => btn.textContent = '🚀 同步到 Pandy', 2000);
}
console.log('🍌 Token 已同步到 Pandy');
} else {
fallbackCopy(btn);
}
},
onerror: function () {
fallbackCopy(btn);
}
});
},
onerror: function () {
fallbackCopy(btn);
}
});
}
function fallbackCopy(btn) {
GM_setClipboard(foundToken);
if (btn) {
btn.textContent = '❌ 服务未启动,已复制';
setTimeout(() => btn.textContent = '🚀 同步到 Pandy', 3000);
}
console.log('🍌 同步失败,请确保 Pandy 服务器已启动 (python server.py)');
}
unsafeWindow.copyToken = copyToken;
unsafeWindow.refreshToken = refreshToken;
unsafeWindow.toggleCollapse = toggleCollapse;
unsafeWindow.syncToPandy = syncToPandy;
function createPanel() {
const panel = document.createElement('div');
panel.id = 'token-grabber-panel';
if (isCollapsed) panel.classList.add('collapsed');
// 设置位置
if (savedPos.x !== null) {
panel.style.left = savedPos.x + 'px';
panel.style.right = 'auto';
}
panel.style.top = savedPos.y + 'px';
panel.innerHTML = `
<style>
#token-grabber-panel {
position: fixed;
top: 10px;
right: 10px;
background: #1a1a2e;
border: 2px solid #f0c040;
border-radius: 10px;
padding: 12px 15px;
z-index: 999999;
font-family: sans-serif;
font-size: 13px;
color: #fff;
width: 260px;
box-shadow: 0 5px 20px rgba(0,0,0,0.5);
transition: width 0.2s, padding 0.2s, border-radius 0.2s;
cursor: default;
}
#token-grabber-panel .header {
display: flex;
align-items: center;
gap: 8px;
cursor: move;
user-select: none;
padding-bottom: 10px;
border-bottom: 1px solid #333;
margin-bottom: 10px;
}
#token-grabber-panel .icon {
font-size: 20px;
}
#token-grabber-panel .title {
flex: 1;
font-weight: bold;
color: #f0c040;
font-size: 14px;
}
#token-grabber-panel .icon-status {
width: 10px;
height: 10px;
border-radius: 50%;
background: #666;
}
#token-grabber-panel .collapse-btn {
cursor: pointer;
color: #888;
font-size: 16px;
padding: 2px 6px;
border-radius: 4px;
}
#token-grabber-panel .collapse-btn:hover {
background: #333;
color: #fff;
}
#token-grabber-panel .content {
transition: opacity 0.2s;
}
#token-grabber-panel .status {
padding: 6px 10px;
border-radius: 6px;
margin: 8px 0;
font-weight: bold;
font-size: 12px;
}
#token-grabber-panel .status.captured { background: rgba(50,200,50,0.2); color: #6f6; }
#token-grabber-panel .status.expired { background: rgba(200,50,50,0.2); color: #f66; }
#token-grabber-panel .token-preview {
font-family: monospace;
font-size: 9px;
color: #666;
word-break: break-all;
background: #111;
padding: 6px;
border-radius: 4px;
margin: 8px 0;
}
#token-grabber-panel #tg-expiry {
color: #6f6;
font-size: 12px;
margin: 6px 0;
}
#token-grabber-panel button {
background: #f0c040;
color: #000;
border: none;
padding: 8px;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
width: 100%;
margin-top: 4px;
font-size: 12px;
}
#token-grabber-panel button:hover { background: #e6a000; }
#token-grabber-panel button.secondary {
background: #333;
color: #aaa;
}
/* 收缩状态 */
#token-grabber-panel.collapsed {
width: auto;
padding: 8px 12px;
border-radius: 25px;
}
#token-grabber-panel.collapsed .header {
padding-bottom: 0;
border-bottom: none;
margin-bottom: 0;
}
#token-grabber-panel.collapsed .content {
display: none;
}
#token-grabber-panel.collapsed .title {
display: none;
}
</style>
<div class="header" id="tg-header">
<span class="icon">🍌</span>
<span class="title">Token Grabber</span>
<span class="icon-status" id="tg-icon-status"></span>
<span class="collapse-btn" onclick="toggleCollapse()" title="收缩/展开">▼</span>
</div>
<div class="content">
<div class="status" id="tg-status">🔍 搜索中...</div>
<div class="token-preview" id="tg-preview">正在搜索...</div>
<div id="tg-expiry"></div>
<button id="tg-copy-btn" onclick="copyToken()">📋 复制 Token</button>
<button id="tg-sync-btn" onclick="syncToPandy()">🚀 同步到 Pandy</button>
<button class="secondary" onclick="refreshToken()">🔄 刷新</button>
</div>
`;
document.body.appendChild(panel);
// 拖拽功能
const header = document.getElementById('tg-header');
let isDragging = false;
let startX, startY, startLeft, startTop;
header.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('collapse-btn')) return;
isDragging = true;
const rect = panel.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
startLeft = rect.left;
startTop = rect.top;
panel.style.right = 'auto';
panel.style.left = startLeft + 'px';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
let newLeft = startLeft + dx;
let newTop = startTop + dy;
// 边界限制
newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - panel.offsetWidth));
newTop = Math.max(0, Math.min(newTop, window.innerHeight - panel.offsetHeight));
panel.style.left = newLeft + 'px';
panel.style.top = newTop + 'px';
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
// 保存位置
const rect = panel.getBoundingClientRect();
savedPos = { x: rect.left, y: rect.top };
GM_setValue(POS_KEY, savedPos);
}
});
}
function init() {
console.log('🍌 Token Grabber: init() 开始执行');
console.log('🍌 document.body:', document.body);
if (!document.body) {
console.log('🍌 body 不存在,延迟重试...');
setTimeout(init, 500);
return;
}
createPanel();
console.log('🍌 面板已创建');
// 确保面板在可视范围内
setTimeout(clampPosition, 100);
// 监听窗口大小变化
window.addEventListener('resize', clampPosition);
setTimeout(() => {
foundToken = searchToken() || GM_getValue(STORAGE_KEY, '');
if (foundToken) GM_setValue(STORAGE_KEY, foundToken);
updatePanel();
console.log('🍌 Token 搜索完成:', foundToken ? '找到' : '未找到');
}, 1500);
setInterval(() => {
const newToken = searchToken();
if (newToken && newToken !== foundToken) {
foundToken = newToken;
GM_setValue(STORAGE_KEY, foundToken);
updatePanel();
}
}, 10000);
}
// 多种方式确保执行
console.log('🍌 Token Grabber: 脚本已加载, readyState:', document.readyState);
if (document.readyState === 'complete') {
init();
} else if (document.readyState === 'interactive') {
setTimeout(init, 100);
} else {
window.addEventListener('load', init);
document.addEventListener('DOMContentLoaded', () => setTimeout(init, 100));
}
})();