Spaces:
Running
Running
| <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> |