| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Puter Token 自动服务</title> |
| <style> |
| body { |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| max-width: 800px; |
| margin: 0 auto; |
| padding: 20px; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| min-height: 100vh; |
| color: white; |
| } |
| .container { |
| background: rgba(255,255,255,0.1); |
| backdrop-filter: blur(10px); |
| padding: 30px; |
| border-radius: 15px; |
| box-shadow: 0 8px 32px rgba(0,0,0,0.3); |
| } |
| .status { |
| padding: 15px; |
| border-radius: 8px; |
| margin: 15px 0; |
| font-weight: 500; |
| } |
| .status.success { background: rgba(40, 167, 69, 0.8); } |
| .status.error { background: rgba(220, 53, 69, 0.8); } |
| .status.info { background: rgba(23, 162, 184, 0.8); } |
| .status.warning { background: rgba(255, 193, 7, 0.8); color: #000; } |
| |
| .token-info { |
| background: rgba(0,0,0,0.3); |
| padding: 20px; |
| border-radius: 10px; |
| margin: 20px 0; |
| font-family: 'Courier New', monospace; |
| word-break: break-all; |
| border-left: 4px solid #00ff88; |
| } |
| |
| .api-endpoints { |
| background: rgba(0,0,0,0.2); |
| padding: 20px; |
| border-radius: 10px; |
| margin: 20px 0; |
| } |
| |
| .endpoint { |
| background: rgba(255,255,255,0.1); |
| padding: 10px; |
| margin: 8px 0; |
| border-radius: 5px; |
| font-family: monospace; |
| } |
| |
| .log-container { |
| background: rgba(0,0,0,0.5); |
| border-radius: 10px; |
| padding: 15px; |
| margin: 20px 0; |
| max-height: 300px; |
| overflow-y: auto; |
| } |
| |
| .log-entry { |
| font-family: 'Courier New', monospace; |
| font-size: 12px; |
| margin: 2px 0; |
| padding: 2px 0; |
| } |
| |
| .log-time { color: #888; } |
| .log-info { color: #00ff88; } |
| .log-error { color: #ff6b6b; } |
| .log-warning { color: #ffd93d; } |
| |
| button { |
| background: rgba(255,255,255,0.2); |
| color: white; |
| border: 2px solid rgba(255,255,255,0.3); |
| padding: 10px 20px; |
| border-radius: 25px; |
| cursor: pointer; |
| margin: 5px; |
| font-weight: 500; |
| transition: all 0.3s ease; |
| } |
| |
| button:hover { |
| background: rgba(255,255,255,0.3); |
| border-color: rgba(255,255,255,0.5); |
| transform: translateY(-2px); |
| } |
| |
| .auto-refresh { |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| margin: 15px 0; |
| flex-wrap: wrap; |
| } |
| |
| .rotation-controls { |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| margin: 15px 0; |
| flex-wrap: wrap; |
| } |
| |
| input[type="checkbox"] { |
| transform: scale(1.2); |
| } |
| |
| select, input[type="number"] { |
| background: rgba(255,255,255,0.2); |
| color: white; |
| border: 1px solid rgba(255,255,255,0.3); |
| padding: 5px 10px; |
| border-radius: 5px; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h1>🔑 Puter Token 自动服务</h1> |
| <p>自动获取和管理 Puter 认证 Token,为其他服务提供 API 访问。</p> |
|
|
| <div id="status" class="status info"> |
| 🚀 服务启动中... |
| </div> |
|
|
| <div class="api-endpoints"> |
| <h3>📡 API 接口</h3> |
| <div class="endpoint">GET http://localhost:8002/token - 获取当前 Token</div> |
| <div class="endpoint">GET http://localhost:8002/status - 检查服务状态</div> |
| <div class="endpoint">POST http://localhost:8002/refresh - 强制刷新 Token</div> |
| </div> |
|
|
| <div id="token-display" class="token-info" style="display: none;"> |
| <h4>🎫 当前 Token 信息</h4> |
| <div><strong>Token:</strong> <span id="token-preview"></span> <button onclick="copyToken()">📋 复制 Token</button></div> |
| <div><strong>获取时间:</strong> <span id="token-time"></span></div> |
| <div><strong>状态:</strong> <span id="token-status"></span></div> |
| </div> |
|
|
| <div class="auto-refresh"> |
| <input type="checkbox" id="auto-refresh" checked> |
| <label for="auto-refresh">自动刷新 Token</label> |
| <div class="rotation-controls"> |
| <label>间隔(分钟): <input id="rotation-minutes" type="number" min="1" max="120" step="1" value="30"></label> |
| <label>触发模型: |
| <select id="rotation-model"> |
| <option value="openrouter:openai/gpt-4o-mini">OpenAI GPT-4o Mini</option> |
| <option value="openrouter:anthropic/claude-3.5-sonnet">Claude 3.5 Sonnet</option> |
| <option value="openrouter:meta-llama/llama-3.3-70b-instruct">Llama 3.3 70B</option> |
| <option value="openrouter:qwen/qwen3-32b">Qwen3 32B</option> |
| </select> |
| </label> |
| <label> |
| <input type="checkbox" id="auto-auth" checked> |
| 自动授权(实验性) |
| </label> |
| </div> |
| <button onclick="manualRefresh()">🔄 强制轮换</button> |
| <button onclick="clearToken()">🗑️ 清除 Token</button> |
| </div> |
|
|
| <div id="auth-prompt" class="status warning" style="display: none;"> |
| ⚠️ 需要授权才能继续轮换 Token |
| <button onclick="handleAuthPrompt()" style="margin-left: 10px;">🔐 立即授权</button> |
| </div> |
|
|
| <div class="log-container"> |
| <h4>📋 服务日志</h4> |
| <div id="log-content"></div> |
| </div> |
| </div> |
|
|
| <script src="https://js.puter.com/v2/"></script> |
| <script> |
| class TokenService { |
| constructor() { |
| this.currentToken = null; |
| this.tokenTime = null; |
| this.autoRefreshInterval = null; |
| this.serverUrl = 'http://localhost:8002'; |
| this.init(); |
| } |
| |
| async init() { |
| this.log('info', '🚀 Token 服务启动'); |
| |
| |
| await this.tryGetToken(); |
| |
| |
| this.startAutoRefresh(); |
| |
| |
| setInterval(() => this.reportToken(), 5000); |
| } |
| |
| async tryGetToken() { |
| try { |
| this.updateStatus('info', '🔍 检查现有 Token...'); |
| |
| |
| if (puter.authToken) { |
| this.currentToken = puter.authToken; |
| this.tokenTime = new Date(); |
| this.log('info', '✅ 发现现有 Token'); |
| this.updateDisplay(); |
| await this.reportToken(); |
| return; |
| } |
| |
| this.updateStatus('warning', '🔐 需要获取新 Token,准备认证...'); |
| this.log('warning', '⚠️ 未找到 Token,开始认证流程'); |
| |
| |
| this.log('info', '🤖 尝试调用 AI 接口触发认证...'); |
| |
| try { |
| |
| const response = await puter.ai.chat('hello', { |
| model: 'openrouter:openai/gpt-4o-mini', |
| stream: false |
| }); |
| |
| |
| if (puter.authToken) { |
| this.currentToken = puter.authToken; |
| this.tokenTime = new Date(); |
| this.log('info', '✅ 认证成功,Token 已获取'); |
| this.updateStatus('success', '✅ Token 获取成功!'); |
| this.updateDisplay(); |
| await this.reportToken(); |
| } else { |
| document.getElementById('auth-prompt').style.display = 'block'; |
| } |
| } catch (error) { |
| document.getElementById('auth-prompt').style.display = 'block'; |
| if (error?.message?.includes('auth')) { |
| this.log('warning', '⚠️ 需要用户授权,请在弹窗中点击授权'); |
| this.updateStatus('warning', '⚠️ 请在弹出的窗口中完成授权'); |
| } else { |
| this.log('error', '❌ 认证失败: ' + (error?.message || error)); |
| this.updateStatus('error', '❌ 认证失败: ' + (error?.message || error)); |
| } |
| } |
| |
| } catch (error) { |
| this.log('error', '❌ Token 获取失败: ' + error.message); |
| this.updateStatus('error', '❌ Token 获取失败: ' + error.message); |
| } |
| } |
| |
| async reportToken() { |
| if (!this.currentToken) return; |
| |
| try { |
| const response = await fetch(this.serverUrl + '/set_token', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ |
| token: this.currentToken, |
| timestamp: this.tokenTime.getTime(), |
| source: 'auto-service' |
| }) |
| }); |
| |
| if (response.ok) { |
| this.log('info', '📤 Token 已上报到服务器'); |
| } else { |
| this.log('warning', '⚠️ Token 上报失败: ' + response.status); |
| } |
| } catch (error) { |
| |
| } |
| } |
| |
| startAutoRefresh() { |
| const checkbox = document.getElementById('auto-refresh'); |
| const minutesInput = document.getElementById('rotation-minutes'); |
| const modelSelect = document.getElementById('rotation-model'); |
| |
| const schedule = () => { |
| if (this.autoRefreshInterval) clearInterval(this.autoRefreshInterval); |
| if (!checkbox.checked) { |
| this.log('info', '⏸️ 自动刷新已禁用'); |
| return; |
| } |
| const minutes = Math.max(1, Math.min(120, parseInt(minutesInput.value || '30', 10))); |
| const model = modelSelect.value; |
| this.autoRefreshInterval = setInterval(() => { |
| this.rotateOnce(model, { userGesture: false }); |
| }, minutes * 60 * 1000); |
| this.log('info', `⏰ 自动刷新已启用 (${minutes} 分钟间隔)`); |
| }; |
| |
| checkbox.addEventListener('change', schedule); |
| minutesInput.addEventListener('change', schedule); |
| modelSelect.addEventListener('change', schedule); |
| schedule(); |
| } |
| |
| async rotateOnce(model, { userGesture = false } = {}) { |
| try { |
| this.log('info', '🔄 自动轮换:重置 token 并触发静默认证'); |
| const autoAuth = document.getElementById('auto-auth')?.checked; |
| |
| |
| if (autoAuth) this.installAuthPopupAutoClicker({ withinUserGesture: userGesture }); |
| |
| |
| puter.resetAuthToken(); |
| |
| if (userGesture) { |
| |
| try { |
| await puter.ui.authenticateWithPuter(); |
| } catch { |
| await puter.ai.chat('ping', { model, stream: false }); |
| } |
| } else { |
| |
| await puter.ai.chat('ping', { model, stream: false }); |
| } |
| |
| |
| if (puter.authToken) { |
| this.currentToken = puter.authToken; |
| this.tokenTime = new Date(); |
| this.updateDisplay(); |
| this.updateStatus('success', '✅ 自动轮换成功,已刷新 Token'); |
| await this.reportToken(); |
| this.log('info', '🔑 当前 Token: ' + this.currentToken); |
| document.getElementById('auth-prompt').style.display = 'none'; |
| } else { |
| this.updateStatus('warning', '⚠️ 自动轮换未获得 Token'); |
| document.getElementById('auth-prompt').style.display = 'block'; |
| } |
| } catch (err) { |
| this.log('warning', '⚠️ 自动轮换失败: ' + (err?.message || err)); |
| document.getElementById('auth-prompt').style.display = 'block'; |
| } |
| } |
| |
| async manualRefresh() { |
| this.log('info', '🔄 强制轮换(用户手势)'); |
| const model = document.getElementById('rotation-model').value; |
| const autoAuth = document.getElementById('auto-auth')?.checked; |
| if (autoAuth) this.installAuthPopupAutoClicker({ withinUserGesture: true }); |
| await this.rotateOnce(model, { userGesture: true }); |
| } |
| |
| clearToken() { |
| this.currentToken = null; |
| this.tokenTime = null; |
| puter.resetAuthToken(); |
| this.updateDisplay(); |
| this.updateStatus('info', '🗑️ Token 已清除'); |
| this.log('info', '🗑️ Token 已清除'); |
| } |
| |
| updateStatus(type, message) { |
| const statusEl = document.getElementById('status'); |
| statusEl.className = `status ${type}`; |
| statusEl.textContent = message; |
| } |
| |
| updateDisplay() { |
| const displayEl = document.getElementById('token-display'); |
| const previewEl = document.getElementById('token-preview'); |
| const timeEl = document.getElementById('token-time'); |
| const statusEl = document.getElementById('token-status'); |
| |
| if (this.currentToken) { |
| displayEl.style.display = 'block'; |
| previewEl.textContent = this.currentToken; |
| timeEl.textContent = this.tokenTime.toLocaleString(); |
| statusEl.textContent = '🟢 有效'; |
| } else { |
| displayEl.style.display = 'none'; |
| } |
| } |
| |
| log(level, message) { |
| const logEl = document.getElementById('log-content'); |
| const timestamp = new Date().toLocaleTimeString(); |
| const entry = document.createElement('div'); |
| entry.className = 'log-entry'; |
| entry.innerHTML = `<span class="log-time">[${timestamp}]</span> <span class="log-${level}">${message}</span>`; |
| logEl.appendChild(entry); |
| logEl.scrollTop = logEl.scrollHeight; |
| |
| |
| while (logEl.children.length > 100) { |
| logEl.removeChild(logEl.firstChild); |
| } |
| } |
| } |
| |
| |
| let tokenService; |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| tokenService = new TokenService(); |
| }); |
| |
| |
| function manualRefresh() { |
| tokenService.manualRefresh(); |
| } |
| |
| function clearToken() { |
| tokenService.clearToken(); |
| } |
| |
| function handleAuthPrompt() { |
| tokenService.rotateOnce(document.getElementById('rotation-model').value, { userGesture: true }); |
| } |
| |
| |
| window.addEventListener('message', (event) => { |
| if (event.data.msg === 'puter.token') { |
| tokenService.log('info', '🎉 收到认证成功事件'); |
| tokenService.tryGetToken(); |
| } |
| function copyToken() { |
| const el = document.getElementById('token-preview'); |
| const text = el?.textContent || ''; |
| if (!text) return; |
| navigator.clipboard.writeText(text).then(() => { |
| alert('已复制 Token 到剪贴板'); |
| }).catch(() => { |
| |
| const ta = document.createElement('textarea'); |
| ta.value = text; |
| document.body.appendChild(ta); |
| ta.select(); |
| try { document.execCommand('copy'); alert('已复制 Token 到剪贴板'); } catch {} |
| document.body.removeChild(ta); |
| }); |
| } |
| }); |
| </script> |
| </body> |
| </html> |
|
|