Files
wechatAiclaw/public/models.html
2026-03-11 11:36:05 +08:00

188 lines
9.5 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>模型管理 - 多模型切换与 API Key</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root {
color-scheme: light dark;
--bg: #0f172a;
--card: #020617;
--border: #1e293b;
--accent: #22c55e;
--accent-soft: rgba(34, 197, 94, 0.12);
--text: #e5e7eb;
--muted: #9ca3af;
--radius: 16px;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top, #1e293b 0, #020617 55%, #000 100%);
font-family: system-ui, -apple-system, sans-serif;
color: var(--text);
}
.nav {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
padding: 12px 24px;
border-bottom: 1px solid var(--border);
background: rgba(2, 6, 23, 0.95);
}
.nav-links { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.nav a { color: var(--muted); text-decoration: none; font-size: 14px; }
.nav a:hover { color: var(--accent); }
.nav a.current { color: var(--accent); font-weight: 500; }
.nav-banner { font-size: 18px; font-weight: 700; letter-spacing: 0.04em; color: var(--text); text-shadow: 0 0 20px rgba(34, 197, 94, 0.2); }
.nav-banner span { color: var(--accent); }
.container { max-width: 720px; margin: 0 auto; padding: 24px; }
.card {
border-radius: var(--radius);
border: 1px solid var(--border);
background: radial-gradient(circle at top, rgba(15, 23, 42, 0.9), #020617);
padding: 20px;
margin-bottom: 20px;
}
.card-title { font-size: 14px; font-weight: 600; margin-bottom: 16px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.small-label { font-size: 12px; color: var(--muted); margin-bottom: 8px; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-bottom: 14px; }
.form-grid .full { grid-column: 1 / -1; }
.form-grid > div { display: flex; flex-direction: column; }
.form-grid label { font-size: 12px; color: var(--muted); display: block; margin-bottom: 4px; }
.form-grid input, .form-grid select {
padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; background: rgba(15,23,42,0.9); color: var(--text); font-size: 13px;
}
.form-grid input[type="password"], .form-grid input[name="api_key"] { font-family: monospace; }
.mgmt-table { width: 100%; font-size: 13px; border-collapse: collapse; }
.mgmt-table th, .mgmt-table td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--border); }
.mgmt-table .current-tag { font-size: 11px; padding: 2px 8px; border-radius: 999px; background: var(--accent-soft); color: var(--accent); }
.primary { padding: 10px 20px; border-radius: 8px; background: var(--accent); color: #000; border: none; cursor: pointer; font-weight: 500; }
.secondary { padding: 6px 12px; border-radius: 8px; background: rgba(30,41,59,0.9); border: 1px solid var(--border); color: var(--text); cursor: pointer; font-size: 12px; margin-right: 6px; }
</style>
</head>
<body>
<nav class="nav">
<div class="nav-banner">Wechat <span>智能托管服务</span></div>
<div class="nav-links">
<a href="manage.html">客户与消息管理</a>
<a href="chat.html">实时消息</a>
<a href="models.html" class="current">模型管理</a>
<a href="swagger.html" target="_blank">API 文档 (Swagger)</a>
</div>
</nav>
<div class="container">
<div class="card">
<div class="card-title">多模型切换 · 填写对应 API Key</div>
<p class="small-label">添加模型后设为「当前使用」,问候语生成、千问生成等将使用当前模型的 API Key 与端点。支持千问DashScope与 OpenAI 兼容接口。</p>
<div class="form-grid" style="margin-top:14px">
<div class="full"><label>显示名称</label><input id="m-name" placeholder="如:千问-turbo、GPT-4" /></div>
<div><label>类型</label><select id="m-provider"><option value="qwen">千问DashScope</option><option value="doubao">豆包Volcengine ARK</option><option value="openai">OpenAI 兼容</option></select></div>
<div class="full"><label>API Key</label><input type="password" id="m-api_key" name="api_key" placeholder="sk-xxx" autocomplete="off" /></div>
<div class="full"><label>Base URL可选千问默认 DashScope</label><input id="m-base_url" placeholder="留空则用默认" /></div>
<div><label>模型名</label><input id="m-model_name" placeholder="如 qwen-turbo、doubao-seed-2-0-pro-260215、gpt-3.5-turbo" /></div>
<div style="display:flex;align-items:flex-end"><label><input type="checkbox" id="m-is_current" /> 设为当前使用</label></div>
</div>
<button type="button" class="primary" id="btn-model-add">添加模型</button>
</div>
<div class="card">
<div class="card-title">已配置模型</div>
<div id="model-list"></div>
</div>
</div>
<script>
const $ = (id) => document.getElementById(id);
// 相对路径,由 Node 代理到后端
const API_BASE = '';
async function callApi(path, options = {}) {
const url = API_BASE + path;
const res = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...(options.headers || {}) } });
let body = null;
try { body = await res.json(); } catch (_) {}
if (!res.ok) throw new Error((body && (body.detail || body.message)) || res.statusText || '请求失败');
return body;
}
async function loadModels() {
try {
const data = await callApi('/api/models');
const list = data.items || [];
const html = list.length ? '<table class="mgmt-table"><thead><tr><th>名称</th><th>类型</th><th>模型</th><th>当前</th><th>操作</th></tr></thead><tbody>' +
list.map(m => '<tr><td>' + (m.name || '-') + '</td><td>' + (m.provider === 'qwen' ? '千问' : m.provider === 'doubao' ? '豆包' : 'OpenAI') + '</td><td>' + (m.model_name || '-') + '</td><td>' + (m.is_current ? '<span class="current-tag">当前</span>' : '<button type="button" class="secondary" data-id="' + m.id + '" data-action="set-current">设为当前</button>') + '</td><td><button type="button" class="secondary" data-id="' + m.id + '" data-action="del">删除</button></td></tr>').join('') + '</tbody></table>' : '<p class="small-label">暂无模型,请在上方添加并填写 API Key。</p>';
$('model-list').innerHTML = html;
$('model-list').querySelectorAll('[data-action="set-current"]').forEach(btn => {
btn.addEventListener('click', async () => {
try {
await callApi('/api/models/' + btn.dataset.id + '/set-current', { method: 'POST' });
loadModels();
} catch (e) { alert('设置失败: ' + e.message); }
});
});
$('model-list').querySelectorAll('[data-action="del"]').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('确定删除该模型?')) return;
try {
await callApi('/api/models/' + btn.dataset.id, { method: 'DELETE' });
loadModels();
} catch (e) { alert('删除失败: ' + e.message); }
});
});
} catch (e) { $('model-list').innerHTML = '<p class="small-label">加载失败: ' + e.message + '</p>'; }
}
async function addModel() {
const name = $('m-name').value.trim();
const provider = $('m-provider').value;
const api_key = $('m-api_key').value.trim();
if (!name || !api_key) { alert('请填写显示名称和 API Key'); return; }
try {
await callApi('/api/models', {
method: 'POST',
body: JSON.stringify({
name,
provider,
api_key,
base_url: $('m-base_url').value.trim() || undefined,
model_name: $('m-model_name').value.trim() || undefined,
is_current: $('m-is_current').checked,
}),
});
$('m-name').value = '';
$('m-api_key').value = '';
$('m-base_url').value = '';
$('m-model_name').value = '';
$('m-is_current').checked = false;
loadModels();
} catch (e) { alert('添加失败: ' + e.message); }
}
$('btn-model-add').addEventListener('click', addModel);
loadModels();
(function wsStatusCheck() {
// 相对路径,由 Node 代理到后端
const API_BASE = '';
let wasConnected = false;
const CHECK_MS = 8000;
const t = setInterval(async () => {
try {
const r = await fetch(API_BASE + '/api/ws-status', { cache: 'no-store' });
const d = await r.json().catch(() => ({}));
if (d && d.connected) wasConnected = true;
if (wasConnected && d && d.connected === false) {
clearInterval(t);
alert('连接已断开,请重新登录');
window.location.href = 'index.html';
}
} catch (_) {}
}, CHECK_MS);
})();
</script>
</body>
</html>