amazonq2api / frontend /index.html
CassiopeiaCode
v2: update frontend/index.html
a393763
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<title>Amazonq2api 前端控制台 · 账号管理 + Chat 测试</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<style>
:root {
--bg:#0a0e1a;
--panel:#0f1420;
--muted:#8b95a8;
--text:#e8f0ff;
--accent:#4f8fff;
--danger:#ff4757;
--ok:#2ed573;
--warn:#ffa502;
--border:#1a2332;
--chip:#141b28;
--code:#0d1218;
--glow:rgba(79,143,255,.15);
}
* { box-sizing:border-box; }
html, body { height:100%; margin:0; }
body {
padding:0 0 40px;
background:radial-gradient(ellipse at top, #0f1624 0%, #0a0e1a 100%);
color:var(--text);
font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Noto Sans,Arial,sans-serif;
line-height:1.6;
}
h1,h2,h3 { font-weight:700; letter-spacing:-.02em; margin:0; }
h1 { font-size:28px; margin:24px 0 12px; background:linear-gradient(135deg,#4f8fff,#7b9fff); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
h2 { font-size:18px; margin:20px 0 16px; color:#c5d4ff; }
h3 { font-size:15px; margin:16px 0 10px; color:#a8b8d8; }
.container { max-width:1280px; margin:0 auto; padding:20px; }
.grid { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
@media(max-width:1024px){ .grid { grid-template-columns:1fr; } }
.panel {
background:linear-gradient(145deg,rgba(15,20,32,.8),rgba(10,14,26,.9));
border:1px solid var(--border);
border-radius:16px;
padding:24px;
box-shadow:0 20px 60px rgba(0,0,0,.4),0 0 0 1px rgba(79,143,255,.08),inset 0 1px 0 rgba(255,255,255,.03);
backdrop-filter:blur(12px);
transition:transform .2s,box-shadow .2s;
}
.panel:hover { transform:translateY(-2px); box-shadow:0 24px 70px rgba(0,0,0,.5),0 0 0 1px rgba(79,143,255,.12),inset 0 1px 0 rgba(255,255,255,.04); }
.row { display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
label { color:var(--muted); font-size:13px; font-weight:500; letter-spacing:.01em; }
.field { display:flex; flex-direction:column; gap:8px; flex:1; min-width:200px; }
input,textarea,select {
background:rgba(12,16,28,.6);
color:var(--text);
border:1px solid var(--border);
border-radius:12px;
padding:12px 14px;
outline:none;
transition:all .2s;
font-size:14px;
box-shadow:inset 0 1px 2px rgba(0,0,0,.2);
}
input:focus,textarea:focus,select:focus {
border-color:var(--accent);
box-shadow:0 0 0 3px var(--glow),inset 0 1px 2px rgba(0,0,0,.2);
background:rgba(12,16,28,.8);
}
textarea { min-height:140px; resize:vertical; font-family:ui-monospace,monospace; }
button {
background:linear-gradient(135deg,#2563eb,#1e40af);
color:#fff;
border:none;
border-radius:12px;
padding:12px 20px;
font-weight:600;
font-size:14px;
cursor:pointer;
transition:all .2s;
box-shadow:0 4px 16px rgba(37,99,235,.3),inset 0 1px 0 rgba(255,255,255,.1);
position:relative;
overflow:hidden;
}
button:before {
content:'';
position:absolute;
top:0;left:0;right:0;bottom:0;
background:linear-gradient(135deg,rgba(255,255,255,.1),transparent);
opacity:0;
transition:opacity .2s;
}
button:hover { transform:translateY(-1px); box-shadow:0 6px 20px rgba(37,99,235,.4),inset 0 1px 0 rgba(255,255,255,.15); }
button:hover:before { opacity:1; }
button:active { transform:translateY(0); }
button:disabled { opacity:.5; cursor:not-allowed; transform:none; }
.btn-secondary { background:linear-gradient(135deg,#1e293b,#0f172a); box-shadow:0 4px 16px rgba(15,23,42,.3),inset 0 1px 0 rgba(255,255,255,.05); }
.btn-secondary:hover { box-shadow:0 6px 20px rgba(15,23,42,.4),inset 0 1px 0 rgba(255,255,255,.08); }
.btn-danger { background:linear-gradient(135deg,#dc2626,#991b1b); box-shadow:0 4px 16px rgba(220,38,38,.3),inset 0 1px 0 rgba(255,255,255,.1); }
.btn-danger:hover { box-shadow:0 6px 20px rgba(220,38,38,.4),inset 0 1px 0 rgba(255,255,255,.15); }
.btn-warn { background:linear-gradient(135deg,#f59e0b,#d97706); box-shadow:0 4px 16px rgba(245,158,11,.3),inset 0 1px 0 rgba(255,255,255,.1); }
.btn-warn:hover { box-shadow:0 6px 20px rgba(245,158,11,.4),inset 0 1px 0 rgba(255,255,255,.15); }
.kvs { display:grid; grid-template-columns:160px 1fr; gap:10px 16px; font-size:13px; }
.muted { color:var(--muted); }
.chip {
display:inline-flex;
align-items:center;
gap:6px;
padding:6px 12px;
background:rgba(20,27,40,.8);
border:1px solid var(--border);
border-radius:20px;
color:#a8b8ff;
font-size:12px;
font-weight:500;
box-shadow:0 2px 8px rgba(0,0,0,.2);
}
.list { display:flex; flex-direction:column; gap:12px; max-height:400px; overflow:auto; padding:2px; }
.list::-webkit-scrollbar { width:8px; }
.list::-webkit-scrollbar-track { background:rgba(0,0,0,.2); border-radius:4px; }
.list::-webkit-scrollbar-thumb { background:rgba(79,143,255,.3); border-radius:4px; }
.list::-webkit-scrollbar-thumb:hover { background:rgba(79,143,255,.5); }
.card {
border:1px solid var(--border);
border-radius:14px;
padding:16px;
background:linear-gradient(145deg,rgba(12,19,34,.6),rgba(10,14,26,.8));
display:flex;
flex-direction:column;
gap:12px;
box-shadow:0 4px 16px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.02);
transition:all .2s;
}
.card:hover { border-color:rgba(79,143,255,.3); box-shadow:0 6px 20px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.03); }
.mono { font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace; }
.code {
background:var(--code);
border:1px solid var(--border);
border-radius:12px;
padding:14px;
color:#d8e8ff;
max-height:300px;
overflow:auto;
white-space:pre-wrap;
font-size:13px;
line-height:1.6;
box-shadow:inset 0 2px 4px rgba(0,0,0,.3);
}
.code::-webkit-scrollbar { width:8px; height:8px; }
.code::-webkit-scrollbar-track { background:rgba(0,0,0,.2); border-radius:4px; }
.code::-webkit-scrollbar-thumb { background:rgba(79,143,255,.3); border-radius:4px; }
.right { margin-left:auto; }
.sep { height:1px; background:linear-gradient(90deg,transparent,rgba(79,143,255,.2),transparent); margin:16px 0; }
.status-ok { color:var(--ok); font-weight:600; }
.status-fail { color:var(--danger); font-weight:600; }
.switch { position:relative; display:inline-block; width:50px; height:26px; }
.switch input { opacity:0; width:0; height:0; }
.slider {
position:absolute;
cursor:pointer;
top:0;left:0;right:0;bottom:0;
background:linear-gradient(135deg,#374151,#1f2937);
transition:.3s;
border-radius:26px;
border:1px solid var(--border);
box-shadow:inset 0 2px 4px rgba(0,0,0,.3);
}
.slider:before {
position:absolute;
content:"";
height:20px;
width:20px;
left:3px;
bottom:2px;
background:linear-gradient(135deg,#f3f4f6,#e5e7eb);
transition:.3s;
border-radius:50%;
box-shadow:0 2px 6px rgba(0,0,0,.3);
}
input:checked+.slider { background:linear-gradient(135deg,#3b82f6,#2563eb); box-shadow:0 0 12px rgba(59,130,246,.4),inset 0 2px 4px rgba(0,0,0,.2); }
input:checked+.slider:before { transform:translateX(24px); }
@keyframes fadeIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
.panel { animation:fadeIn .4s ease-out; }
.tabs { display:flex; gap:8px; margin:20px 0 16px; border-bottom:2px solid var(--border); }
.tab { padding:12px 24px; background:transparent; border:none; color:var(--muted); font-weight:600; font-size:14px; cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-2px; transition:all .2s; }
.tab:hover { color:var(--text); background:rgba(79,143,255,.05); }
.tab.active { color:var(--accent); border-bottom-color:var(--accent); }
.tab-content { display:none; }
.tab-content.active { display:block; }
</style>
</head>
<body>
<div class="container">
<h1>v2 前端控制台</h1>
<div class="tabs">
<button class="tab active" onclick="switchTab('accounts')">账号管理</button>
<button class="tab" onclick="switchTab('create')">创建账号</button>
<button class="tab" onclick="switchTab('login')">URL登录</button>
<button class="tab" onclick="switchTab('chat')">Chat测试</button>
</div>
<div id="tab-accounts" class="tab-content active">
<div class="panel">
<h2>账号管理</h2>
<div class="row">
<button class="btn-secondary" onclick="loadAccounts()">刷新列表</button>
</div>
<div class="list" id="accounts"></div>
</div>
</div>
<div id="tab-create" class="tab-content">
<div class="panel">
<h2>创建账号</h2>
<div class="row">
<div class="field"><label>label</label><input id="new_label" /></div>
<div class="field"><label>clientId</label><input id="new_clientId" /></div>
<div class="field"><label>clientSecret</label><input id="new_clientSecret" /></div>
</div>
<div class="row">
<div class="field"><label>refreshToken</label><input id="new_refreshToken" /></div>
<div class="field"><label>accessToken</label><input id="new_accessToken" /></div>
</div>
<div class="row">
<div class="field">
<label>other(JSON,可选)</label>
<textarea id="new_other" placeholder='{"note":"备注"}'></textarea>
</div>
<div class="field" style="max-width:220px">
<label>启用(仅启用账号会被用于请求)</label>
<div>
<label class="switch">
<input id="new_enabled" type="checkbox" checked />
<span class="slider"></span>
</label>
</div>
</div>
</div>
<div class="row">
<button onclick="createAccount()">创建</button>
</div>
</div>
</div>
<div id="tab-login" class="tab-content">
<div class="panel">
<h2>URL 登录(5分钟超时)</h2>
<div class="row">
<div class="field"><label>label(可选)</label><input id="auth_label" /></div>
<div class="field" style="max-width:220px">
<label>启用(登录成功后新账号是否启用)</label>
<div>
<label class="switch">
<input id="auth_enabled" type="checkbox" checked />
<span class="slider"></span>
</label>
</div>
</div>
</div>
<div class="row">
<button onclick="startAuth()">开始登录</button>
<button class="btn-secondary" onclick="claimAuth()">等待授权并创建账号</button>
</div>
<div class="field">
<label>登录信息</label>
<pre class="code mono" id="auth_info">尚未开始</pre>
</div>
</div>
</div>
<div id="tab-chat" class="tab-content">
<div class="panel">
<h2>Chat 测试(OpenAI 兼容 /v1/chat/completions)</h2>
<div class="row">
<div class="field" style="max-width:300px">
<label>model</label>
<input id="model" value="claude-sonnet-4" />
</div>
<div class="field" style="max-width:180px">
<label>是否流式</label>
<select id="stream">
<option value="false">false(默认)</option>
<option value="true">true(SSE)</option>
</select>
</div>
<button class="right" onclick="send()">发送请求</button>
</div>
<div class="field">
<label>messages(JSON)</label>
<textarea id="messages">[
{"role":"system","content":"你是一个乐于助人的助手"},
{"role":"user","content":"你好,请讲一个简短的故事"}
]</textarea>
</div>
<div class="field">
<label>响应</label>
<pre class="code mono" id="out"></pre>
</div>
</div>
</div>
</div>
<script>
function switchTab(name) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('tab-' + name).classList.add('active');
}
function api(path){
const pathClean = ('/' + (path || '').replace(/^\/+/, '')).replace(/\/{2,}/g, '/');
return pathClean;
}
function renderAccounts(list){
const root = document.getElementById('accounts');
root.innerHTML = '';
if (!Array.isArray(list) || list.length === 0) {
const empty = document.createElement('div');
empty.className = 'muted';
empty.textContent = '暂无账号';
root.appendChild(empty);
return;
}
for (const acc of list) {
const card = document.createElement('div');
card.className = 'card';
const header = document.createElement('div');
header.className = 'row';
const name = document.createElement('div');
name.innerHTML = '<strong>' + (acc.label || '(无标签)') + '</strong>';
const id = document.createElement('div');
id.className = 'chip mono';
id.textContent = acc.id;
const spacer = document.createElement('div');
spacer.className = 'right';
// Enabled toggle
const toggleWrap = document.createElement('div');
const toggleLabel = document.createElement('label');
toggleLabel.style.marginRight = '6px';
toggleLabel.className = 'muted';
toggleLabel.textContent = '启用';
const toggle = document.createElement('label');
toggle.className = 'switch';
const chk = document.createElement('input');
chk.type = 'checkbox';
chk.checked = !!acc.enabled;
chk.onchange = async () => {
try {
await updateAccount(acc.id, { enabled: chk.checked });
} catch(e) {
// revert if failed
chk.checked = !chk.checked;
}
};
const slider = document.createElement('span');
slider.className = 'slider';
toggle.appendChild(chk); toggle.appendChild(slider);
toggleWrap.appendChild(toggleLabel); toggleWrap.appendChild(toggle);
const refreshBtn = document.createElement('button');
refreshBtn.className = 'btn-warn';
refreshBtn.textContent = '刷新Token';
refreshBtn.onclick = () => refreshAccount(acc.id);
const delBtn = document.createElement('button');
delBtn.className = 'btn-danger';
delBtn.textContent = '删除';
delBtn.onclick = () => deleteAccount(acc.id);
header.appendChild(name);
header.appendChild(id);
header.appendChild(spacer);
header.appendChild(toggleWrap);
header.appendChild(refreshBtn);
header.appendChild(delBtn);
card.appendChild(header);
const meta = document.createElement('div');
meta.className = 'kvs mono';
function row(k, v) {
const kEl = document.createElement('div'); kEl.className = 'muted'; kEl.textContent = k;
const vEl = document.createElement('div'); vEl.textContent = v ?? '';
meta.appendChild(kEl); meta.appendChild(vEl);
}
row('enabled', String(!!acc.enabled));
row('success_count', acc.success_count ?? 0);
row('error_count', acc.error_count ?? 0);
row('last_refresh_status', acc.last_refresh_status);
row('last_refresh_time', acc.last_refresh_time);
row('clientId', acc.clientId);
row('hasRefreshToken', acc.refreshToken ? 'yes' : 'no');
row('hasAccessToken', acc.accessToken ? 'yes' : 'no');
row('created_at', acc.created_at);
row('updated_at', acc.updated_at);
if (acc.other) {
row('other', JSON.stringify(acc.other));
}
card.appendChild(meta);
// quick edit form (label, accessToken)
const editRow = document.createElement('div');
editRow.className = 'row';
editRow.style.marginTop = '8px';
const labelField = document.createElement('input');
labelField.placeholder = 'label';
labelField.value = acc.label || '';
const accessField = document.createElement('input');
accessField.placeholder = 'accessToken(可选)';
accessField.value = acc.accessToken || '';
const saveBtn = document.createElement('button');
saveBtn.className = 'btn-secondary';
saveBtn.textContent = '保存';
saveBtn.onclick = async () => {
await updateAccount(acc.id, { label: labelField.value, accessToken: accessField.value });
};
editRow.appendChild(labelField);
editRow.appendChild(accessField);
editRow.appendChild(saveBtn);
card.appendChild(editRow);
root.appendChild(card);
}
}
async function loadAccounts(){
try{
const r = await fetch(api('/v2/accounts'));
const j = await r.json();
renderAccounts(j);
} catch(e){
alert('加载账户失败:' + e);
}
}
async function createAccount(){
const body = {
label: document.getElementById('new_label').value.trim() || null,
clientId: document.getElementById('new_clientId').value.trim(),
clientSecret: document.getElementById('new_clientSecret').value.trim(),
refreshToken: document.getElementById('new_refreshToken').value.trim() || null,
accessToken: document.getElementById('new_accessToken').value.trim() || null,
enabled: document.getElementById('new_enabled').checked,
other: (()=>{
const t = document.getElementById('new_other').value.trim();
if (!t) return null;
try { return JSON.parse(t); } catch { alert('other 不是合法 JSON'); throw new Error('bad other'); }
})()
};
try{
const r = await fetch(api('/v2/accounts'), {
method:'POST',
headers:{ 'content-type':'application/json' },
body: JSON.stringify(body)
});
if (!r.ok) {
const t = await r.text();
throw new Error(t);
}
await loadAccounts();
} catch(e){
alert('创建失败:' + e);
}
}
async function deleteAccount(id){
if (!confirm('确认删除该账号?')) return;
try{
const r = await fetch(api('/v2/accounts/' + encodeURIComponent(id)), { method:'DELETE' });
if (!r.ok) { throw new Error(await r.text()); }
await loadAccounts();
} catch(e){
alert('删除失败:' + e);
}
}
async function updateAccount(id, patch){
try{
const r = await fetch(api('/v2/accounts/' + encodeURIComponent(id)), {
method:'PATCH',
headers:{ 'content-type':'application/json' },
body: JSON.stringify(patch)
});
if (!r.ok) { throw new Error(await r.text()); }
await loadAccounts();
} catch(e){
alert('更新失败:' + e);
}
}
async function refreshAccount(id){
try{
const r = await fetch(api('/v2/accounts/' + encodeURIComponent(id) + '/refresh'), { method:'POST' });
if (!r.ok) { throw new Error(await r.text()); }
await loadAccounts();
} catch(e){
alert('刷新失败:' + e);
}
}
// URL Login (Device Authorization)
let currentAuth = null;
async function startAuth(){
const body = {
label: (document.getElementById('auth_label').value || '').trim() || null,
enabled: document.getElementById('auth_enabled').checked
};
try {
const r = await fetch(api('/v2/auth/start'), {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body)
});
if (!r.ok) throw new Error(await r.text());
const j = await r.json();
currentAuth = j;
const info = [
'验证链接: ' + j.verificationUriComplete,
'用户代码: ' + (j.userCode || ''),
'authId: ' + j.authId,
'expiresIn: ' + j.expiresIn + 's',
'interval: ' + j.interval + 's'
].join('\\n');
const el = document.getElementById('auth_info');
el.textContent = info + '\\n\\n请在新窗口中打开上述链接完成登录。';
try { window.open(j.verificationUriComplete, '_blank'); } catch {}
} catch(e){
document.getElementById('auth_info').textContent = '启动失败:' + e;
}
}
async function claimAuth(){
if (!currentAuth || !currentAuth.authId) {
document.getElementById('auth_info').textContent = '请先点击“开始登录”。';
return;
}
document.getElementById('auth_info').textContent += '\\n\\n正在等待授权并创建账号(最多5分钟)...';
try{
const r = await fetch(api('/v2/auth/claim/' + encodeURIComponent(currentAuth.authId)), { method: 'POST' });
const text = await r.text();
let j;
try { j = JSON.parse(text); } catch { j = { raw: text }; }
document.getElementById('auth_info').textContent = '完成:\\n' + JSON.stringify(j, null, 2);
await loadAccounts();
} catch(e){
document.getElementById('auth_info').textContent += '\\n失败:' + e;
}
}
async function send() {
const model = document.getElementById('model').value.trim();
const stream = document.getElementById('stream').value === 'true';
const out = document.getElementById('out');
out.textContent = '';
let messages;
try { messages = JSON.parse(document.getElementById('messages').value); }
catch(e){ out.textContent = 'messages 不是合法 JSON'; return; }
const body = { model, messages, stream };
const headers = { 'content-type': 'application/json' };
if (!stream) {
const r = await fetch(api('/v1/chat/completions'), {
method:'POST',
headers,
body: JSON.stringify(body)
});
const text = await r.text();
try { out.textContent = JSON.stringify(JSON.parse(text), null, 2); }
catch { out.textContent = text; }
} else {
const r = await fetch(api('/v1/chat/completions'), {
method:'POST',
headers,
body: JSON.stringify(body)
});
const reader = r.body.getReader();
const decoder = new TextDecoder();
while (true) {
const {value, done} = await reader.read();
if (done) break;
out.textContent += decoder.decode(value, {stream:true});
}
}
}
window.addEventListener('DOMContentLoaded', () => {
loadAccounts();
});
</script>
</body>
</html>