PETER / static /token-service.html
partybobo's picture
Update static/token-service.html
ca44979 verified
Raw
History Blame Contribute Delete
18.2 kB
<!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 服务启动');
// 尝试自动获取 token
await this.tryGetToken();
// 启动自动刷新
this.startAutoRefresh();
// 定期检查并上报 token
setInterval(() => this.reportToken(), 5000);
}
async tryGetToken() {
try {
this.updateStatus('info', '🔍 检查现有 Token...');
// 检查是否已有 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,开始认证流程');
// 尝试通过调用 AI 接口触发认证
this.log('info', '🤖 尝试调用 AI 接口触发认证...');
try {
// 这会自动触发认证弹窗
const response = await puter.ai.chat('hello', {
model: 'openrouter:openai/gpt-4o-mini',
stream: false
});
// 如果成功,说明已经有 token 了
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 });
// 重置本地 token
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 });
}
// 监听 Puter 认证事件
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>