| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Augment2Api</title> |
| <style> |
| :root { |
| --primary-color: #4361ee; |
| --secondary-color: #3f37c9; |
| --success-color: #4caf50; |
| --error-color: #f44336; |
| --bg-color: #f8f9fa; |
| --card-bg: #ffffff; |
| --text-color: #333333; |
| --border-color: #e0e0e0; |
| --shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
| --radius: 8px; |
| } |
| |
| * { |
| box-sizing: border-box; |
| margin: 0; |
| padding: 0; |
| } |
| |
| body { |
| font-family: 'PingFang SC', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; |
| background-color: var(--bg-color); |
| color: var(--text-color); |
| line-height: 1.6; |
| padding: 20px; |
| min-height: 100vh; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| header { |
| text-align: center; |
| margin-bottom: 30px; |
| padding-bottom: 15px; |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| h1 { |
| font-size: 28px; |
| font-weight: 600; |
| color: black; |
| } |
| |
| h2 { |
| font-size: 20px; |
| font-weight: 500; |
| margin-bottom: 15px; |
| color: var(--secondary-color); |
| } |
| |
| .dashboard { |
| display: flex; |
| flex-direction: row; |
| gap: 20px; |
| align-items: stretch; |
| min-height: 500px; |
| } |
| |
| @media (max-width: 768px) { |
| .dashboard { |
| flex-direction: column; |
| } |
| } |
| |
| .panel { |
| background: var(--card-bg); |
| border-radius: var(--radius); |
| box-shadow: var(--shadow); |
| padding: 25px; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .panel-left { |
| width: 50%; |
| max-width: 580px; |
| flex: 1; |
| } |
| |
| .panel-right { |
| width: 50%; |
| max-width: 580px; |
| flex: 1; |
| position: sticky; |
| top: 20px; |
| } |
| |
| @media (max-width: 768px) { |
| .panel-left, .panel-right { |
| width: 100%; |
| max-width: none; |
| } |
| } |
| |
| .panel-title { |
| display: flex; |
| align-items: center; |
| margin-bottom: 20px; |
| } |
| |
| .panel-title i { |
| margin-right: 10px; |
| font-size: 20px; |
| color: var(--primary-color); |
| } |
| |
| .panel-title h2 { |
| margin-bottom: 0; |
| } |
| |
| .token-display { |
| background: var(--bg-color); |
| padding: 15px; |
| border-radius: var(--radius); |
| word-break: break-all; |
| margin: 15px 0; |
| border: 1px solid var(--border-color); |
| font-family: monospace; |
| font-size: 14px; |
| position: relative; |
| } |
| |
| .token-label { |
| font-weight: 500; |
| margin-bottom: 5px; |
| color: #666; |
| } |
| |
| button { |
| background: var(--primary-color); |
| color: white; |
| border: none; |
| padding: 10px 15px; |
| border-radius: var(--radius); |
| cursor: pointer; |
| font-size: 14px; |
| margin-top: 10px; |
| transition: background 0.3s; |
| display: inline-flex; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| button:hover { |
| background: var(--secondary-color); |
| } |
| |
| button i { |
| margin-right: 8px; |
| } |
| |
| input, textarea { |
| width: 100%; |
| padding: 12px; |
| margin: 10px 0; |
| border: 1px solid var(--border-color); |
| border-radius: var(--radius); |
| font-size: 14px; |
| transition: border 0.3s; |
| } |
| |
| input:focus, textarea:focus { |
| outline: none; |
| border-color: var(--primary-color); |
| } |
| |
| textarea { |
| height: 120px; |
| resize: vertical; |
| } |
| |
| .error { |
| color: var(--error-color); |
| margin: 10px 0; |
| padding: 10px; |
| background-color: rgba(244, 67, 54, 0.1); |
| border-radius: var(--radius); |
| display: none; |
| } |
| |
| .success { |
| color: var(--success-color); |
| margin: 10px 0; |
| padding: 10px; |
| background-color: rgba(76, 175, 80, 0.1); |
| border-radius: var(--radius); |
| display: none; |
| } |
| |
| .step { |
| margin-bottom: 20px; |
| padding-bottom: 20px; |
| border-bottom: 1px dashed var(--border-color); |
| } |
| |
| .step:last-child { |
| border-bottom: none; |
| } |
| |
| .step-number { |
| display: inline-block; |
| width: 24px; |
| height: 24px; |
| background-color: var(--primary-color); |
| color: white; |
| border-radius: 50%; |
| text-align: center; |
| line-height: 24px; |
| margin-right: 10px; |
| font-size: 14px; |
| } |
| |
| |
| .icon { |
| display: inline-block; |
| width: 1em; |
| height: 1em; |
| stroke-width: 0; |
| stroke: currentColor; |
| fill: currentColor; |
| vertical-align: middle; |
| } |
| |
| |
| .token-list { |
| display: flex; |
| flex-direction: column; |
| gap: 15px; |
| margin-bottom: 15px; |
| max-height: 500px; |
| overflow-y: auto; |
| padding-right: 10px; |
| } |
| |
| .token-item { |
| background: var(--bg-color); |
| border-radius: var(--radius); |
| border: 1px solid var(--border-color); |
| overflow: hidden; |
| transition: all 0.3s ease; |
| } |
| |
| .current-token { |
| border: 2px solid var(--primary-color); |
| } |
| |
| .token-header { |
| display: flex; |
| padding: 12px 15px; |
| cursor: pointer; |
| align-items: center; |
| background: #f0f2f5; |
| } |
| |
| .token-number { |
| background: var(--primary-color); |
| color: white; |
| padding: 6px 10px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-weight: bold; |
| min-width: 30px; |
| border-radius: 4px; |
| margin-right: 12px; |
| } |
| |
| .token-summary { |
| flex: 1; |
| font-weight: 500; |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| } |
| |
| .token-toggle { |
| margin-left: 10px; |
| transition: transform 0.3s; |
| } |
| |
| .token-toggle.open { |
| transform: rotate(180deg); |
| } |
| |
| .token-details { |
| padding: 0; |
| max-height: 0; |
| overflow: hidden; |
| transition: all 0.3s ease; |
| } |
| |
| .token-details.open { |
| padding: 12px; |
| max-height: none; |
| overflow: visible; |
| } |
| |
| .token-details .token-label { |
| font-size: 13px; |
| margin-bottom: 3px; |
| } |
| |
| .token-details .token-display { |
| font-size: 13px; |
| padding: 10px; |
| margin: 5px 0 10px 0; |
| } |
| |
| .token-actions { |
| display: flex; |
| justify-content: flex-end; |
| margin-top: 8px; |
| } |
| |
| .token-actions button { |
| margin-left: 10px; |
| padding: 6px 12px; |
| font-size: 13px; |
| } |
| |
| .no-tokens { |
| padding: 20px; |
| text-align: center; |
| background: var(--bg-color); |
| border-radius: var(--radius); |
| color: #666; |
| margin-bottom: 15px; |
| } |
| |
| |
| .loading { |
| position: relative; |
| pointer-events: none; |
| } |
| |
| .loading:after { |
| content: ''; |
| display: inline-block; |
| width: 1em; |
| height: 1em; |
| border: 2px solid rgba(255, 255, 255, 0.3); |
| border-radius: 50%; |
| border-top-color: white; |
| animation: spin 0.8s linear infinite; |
| margin-left: 8px; |
| vertical-align: middle; |
| } |
| |
| @keyframes spin { |
| to { |
| transform: rotate(360deg); |
| } |
| } |
| |
| .btn-text { |
| display: inline-block; |
| } |
| |
| .loading .btn-icon { |
| display: none; |
| } |
| |
| |
| footer { |
| text-align: center; |
| margin-top: 30px; |
| padding: 15px 0; |
| color: #666; |
| font-size: 14px; |
| border-top: 1px solid var(--border-color); |
| margin-top: auto; |
| } |
| |
| footer a { |
| color: var(--primary-color); |
| text-decoration: none; |
| transition: color 0.3s; |
| } |
| |
| footer a:hover { |
| color: var(--secondary-color); |
| text-decoration: underline; |
| } |
| </style> |
| |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"> |
| </head> |
| <body> |
| <div class="container"> |
| <header> |
| <h1>Augment面板</h1> |
| </header> |
|
|
| <div class="dashboard"> |
| |
| <div class="panel panel-left"> |
| <div class="panel-title"> |
| <i class="bi bi-key-fill"></i> |
| <h2>当前Token列表</h2> |
| </div> |
| |
| <div id="token-list">加载中...</div> |
| <button id="refresh-token"><i class="bi bi-arrow-clockwise btn-icon"></i> <span class="btn-text">刷新</span></button> |
| </div> |
|
|
| |
| <div class="panel panel-right"> |
| <div class="panel-title"> |
| <i class="bi bi-shield-lock-fill"></i> |
| <h2>授权获取Token</h2> |
| </div> |
|
|
| <div class="auth-steps"> |
| <div class="step"> |
| <h3><span class="step-number">1</span> 获取授权地址</h3> |
| <p>点击下方按钮获取授权地址,然后在浏览器中打开该地址进行授权。</p> |
| <button id="get-auth-url"><i class="bi bi-link-45deg" class="btn-icon"></i> <span class="btn-text">获取授权地址</span></button> |
| <div id="auth-url" class="token-display" style="display: none;"></div> |
| </div> |
|
|
| <div class="step"> |
| <h3><span class="step-number">2</span> 提交授权响应</h3> |
| <p>完成授权后,将获得的授权响应粘贴到下面的文本框中:</p> |
| <textarea id="auth-response" placeholder='{"code":"_000baec407c57c4bf9xxxxxxxxxxxxxx","state":"0uXxxxxxxxx","tenant_url":"https://d20.api.augmentcode.com/"}'></textarea> |
| <div id="validation-message" class="error"></div> |
| <button id="submit-auth"><i class="bi bi-check2-circle" class="btn-icon"></i> <span class="btn-text">获取Token</span></button> |
| <div id="submit-result" class="success"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <footer> |
| Designed by <a href="https://linux.do/u/bifang/summary" target="_blank">彼方</a> |
| </footer> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| function fetchCurrentToken() { |
| fetch('/api/tokens') |
| .then(response => response.json()) |
| .then(data => { |
| if (data.status === 'success') { |
| const tokenListElement = document.getElementById('token-list'); |
| |
| if (data.tokens.length === 0) { |
| tokenListElement.innerHTML = '<div class="no-tokens">暂无可用Token,请点击右侧面板获取</div>'; |
| return; |
| } |
| |
| |
| tokenListElement.innerHTML = '<div class="token-list"></div>'; |
| const listContainer = tokenListElement.querySelector('.token-list'); |
| |
| |
| data.tokens.forEach((tokenInfo, index) => { |
| |
| const shortToken = tokenInfo.token.length > 15 |
| ? tokenInfo.token.substring(0, 15) + '...' |
| : tokenInfo.token; |
| |
| const tokenItem = document.createElement('div'); |
| tokenItem.className = 'token-item'; |
| tokenItem.innerHTML = ` |
| <div class="token-header"> |
| <div class="token-number">${index + 1}</div> |
| <div class="token-summary">${shortToken}</div> |
| <div class="token-toggle"><i class="bi bi-chevron-down"></i></div> |
| </div> |
| <div class="token-details"> |
| <div class="token-label">Token:</div> |
| <div class="token-display">${tokenInfo.token}</div> |
| <div class="token-label">租户URL:</div> |
| <div class="token-display">${tokenInfo.tenant_url}</div> |
| <div class="token-actions"> |
| <button class="delete-token" data-token="${tokenInfo.token}"> |
| <i class="bi bi-trash"></i> 删除 |
| </button> |
| </div> |
| </div> |
| `; |
| |
| listContainer.appendChild(tokenItem); |
| |
| |
| const header = tokenItem.querySelector('.token-header'); |
| const details = tokenItem.querySelector('.token-details'); |
| const toggle = tokenItem.querySelector('.token-toggle'); |
| |
| header.addEventListener('click', function() { |
| details.classList.toggle('open'); |
| toggle.classList.toggle('open'); |
| }); |
| }); |
| } else { |
| showError('获取Token列表失败: ' + (data.error || '未知错误')); |
| } |
| }) |
| .catch(error => { |
| document.getElementById('token-list').innerHTML = |
| '<div class="error" style="display:block;">请求失败: ' + error.message + '</div>'; |
| }); |
| |
| return Promise.resolve(); |
| } |
| |
| |
| function showError(message) { |
| const tokenListElement = document.getElementById('token-list'); |
| tokenListElement.innerHTML = `<div class="error" style="display:block;">${message}</div>`; |
| } |
| |
| |
| document.getElementById('get-auth-url').addEventListener('click', function() { |
| const button = this; |
| button.classList.add('loading'); |
| |
| fetch('/auth') |
| .then(response => response.json()) |
| .then(data => { |
| const authUrlElement = document.getElementById('auth-url'); |
| authUrlElement.textContent = data.authorize_url; |
| authUrlElement.style.display = 'block'; |
| }) |
| .catch(error => { |
| alert('获取授权地址失败: ' + error.message); |
| }) |
| .finally(() => { |
| button.classList.remove('loading'); |
| }); |
| }); |
| |
| |
| function validateAuthResponse(response) { |
| try { |
| const data = JSON.parse(response); |
| if (!data.code || !data.state || !data.tenant_url) { |
| return { valid: false, message: '缺少必要字段: code, state 或 tenant_url' }; |
| } |
| return { valid: true }; |
| } catch (e) { |
| return { valid: false, message: 'JSON格式无效: ' + e.message }; |
| } |
| } |
| |
| |
| document.getElementById('submit-auth').addEventListener('click', function() { |
| const button = this; |
| const authResponse = document.getElementById('auth-response').value.trim(); |
| const validationMessage = document.getElementById('validation-message'); |
| |
| |
| if (!authResponse) { |
| validationMessage.textContent = '请输入授权响应'; |
| validationMessage.style.display = 'block'; |
| return; |
| } |
| |
| const validationResult = validateAuthResponse(authResponse); |
| |
| if (!validationResult.valid) { |
| validationMessage.textContent = validationResult.message; |
| validationMessage.style.display = 'block'; |
| return; |
| } |
| |
| validationMessage.style.display = 'none'; |
| button.classList.add('loading'); |
| |
| fetch('/callback', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: authResponse |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| const submitResult = document.getElementById('submit-result'); |
| if (data.status === 'success') { |
| submitResult.textContent = 'Token获取成功!'; |
| submitResult.className = 'success'; |
| fetchCurrentToken(); |
| } else { |
| submitResult.textContent = '获取失败: ' + (data.error || '未知错误'); |
| submitResult.className = 'error'; |
| } |
| submitResult.style.display = 'block'; |
| }) |
| .catch(error => { |
| const submitResult = document.getElementById('submit-result'); |
| submitResult.textContent = '请求失败: ' + error.message; |
| submitResult.className = 'error'; |
| submitResult.style.display = 'block'; |
| }) |
| .finally(() => { |
| button.classList.remove('loading'); |
| }); |
| }); |
| |
| |
| document.getElementById('refresh-token').addEventListener('click', function() { |
| const button = this; |
| button.classList.add('loading'); |
| |
| fetchCurrentToken() |
| .finally(() => { |
| button.classList.remove('loading'); |
| }); |
| }); |
| |
| |
| document.addEventListener('click', function(e) { |
| if (e.target.closest('.delete-token')) { |
| const button = e.target.closest('.delete-token'); |
| const token = button.dataset.token; |
| |
| if (confirm('确定要删除此Token吗?')) { |
| fetch(`/api/token/${token}`, { |
| method: 'DELETE' |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| if (data.status === 'success') { |
| fetchCurrentToken(); |
| } else { |
| alert('删除失败: ' + (data.error || '未知错误')); |
| } |
| }) |
| .catch(error => { |
| alert('请求失败: ' + error.message); |
| }); |
| } |
| } |
| }); |
| |
| |
| fetchCurrentToken(); |
| }); |
| </script> |
| </body> |
| </html> |