用一张卡片替代原来的两个独立区块,统一管理模型选择、添加、配置和删除。
HTML 结构
<!-- ═══════════ 模型管理(合并 /model + /set + /model add) ═══════════ -->
<fieldset class="settings-fieldset">
<legend>模型管理 <span class="legend-hint">(/model)</span></legend>
<p style="color:var(--text-weak);font-size:12px;margin:0 0 14px 0;">
选择使用中的模型,或添加新的 OpenAI 兼容 API 端点。
</p>
<!-- 模型列表(每个模型一行:单选 · 名称 · 操作) -->
<div id="settings-models-list" style="max-height:320px;overflow-y:auto;margin-bottom:14px;">
<!-- JS 动态填充 -->
</div>
<!-- 添加新模型表单(默认折叠) -->
<details id="add-model-details" style="border-top:1px solid var(--border-base);padding-top:12px;">
<summary style="cursor:pointer;color:var(--brand);font-size:13px;font-weight:600;user-select:none;">
+ 添加模型
</summary>
<div style="margin-top:10px;">
<div class="field-row">
<input id="new-model-id" placeholder="模型ID" style="flex:1;">
<input id="new-model-name" placeholder="显示名称" style="flex:1;">
</div>
<div class="field-row" style="margin-top:8px;">
<input id="new-model-url" placeholder="API 地址" style="flex:2;">
<input id="new-model-key" type="password" placeholder="API Key" style="flex:1;">
</div>
<div class="field-row" style="margin-top:10px;">
<button class="btn btn-primary" onclick="addModel()" style="flex:1;">添加模型</button>
</div>
</div>
</details>
</fieldset>
核心 JS 函数
renderModelList — 模型列表渲染
async function renderModelList() {
const container = document.getElementById('settings-models-list');
container.innerHTML = '加载中…';
try {
const res = await fetch(API_BASE + '/api/models');
const data = await res.json();
const models = data.models || [];
if (models.length === 0) {
container.innerHTML = '<div style="padding:12px;color:var(--text-weak);">暂无模型,请在下方添加。</div>';
return;
}
container.innerHTML = models.map((m, i) => {
const isActive = (currentModel === m.id);
const badge = m.custom
? '<span style="font-size:11px;background:var(--brand-soft);color:var(--brand);padding:2px 8px;border-radius:10px;margin-left:6px;">自定义</span>'
: '';
const rowStyle = `
display:flex; align-items:center; gap:10px;
padding:10px 12px; border-radius:var(--radius-sm);
background:${isActive ? 'var(--brand-soft)' : 'transparent'};
border:1px solid ${isActive ? 'var(--brand)' : 'var(--border-base)'};
margin-bottom:8px; transition:border-color .2s;
`;
return `<div style="${rowStyle}" id="model-row-${i}">
<input type="radio" name="active-model" value="${m.id}"
${isActive ? 'checked' : ''}
onchange="activateModel('${m.id}')"
style="accent-color:var(--brand); width:16px; height:16px; cursor:pointer; flex-shrink:0;"
title="选择此模型">
<div style="flex:1; min-width:0;">
<div style="font-weight:600; color:var(--text-strong);">${m.name}${badge}</div>
<div style="font-size:12px; color:var(--text-weak); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${m.id} · ${m.vendor || m.baseUrl || ''}</div>
</div>
<button class="btn btn-secondary" style="padding:4px 10px; font-size:12px;"
onclick="editModel('${m.id}', '${m.name}', '${m.baseUrl || ''}', ${m.custom || false})">✎</button>
${m.custom ? `<button class="btn btn-secondary" style="padding:4px 10px; font-size:12px; color:var(--error);"
onclick="deleteModel('${m.customId || m.id}')">✕</button>` : ''}
</div>`;
}).join('');
} catch (e) {
container.innerHTML = '<div style="padding:12px;color:var(--error);">加载模型列表失败</div>';
}
}
activateModel — 激活选中的模型
function activateModel(modelId) {
currentModel = modelId;
localStorage.setItem('cmdcode_current_model', modelId);
// 自动同步 Base URL
const matched = modelsCache.find(m => m.id === modelId);
if (matched && matched.url) {
document.getElementById('settings-baseurl').value = matched.url;
localStorage.setItem('cmdcode_baseurl', matched.url);
}
// 同步 Header 下拉框
const headerSelect = document.querySelector('.header .model-select');
if (headerSelect) headerSelect.value = modelId;
renderModelList(); // 刷新高亮
showToast('已切换至: ' + modelId);
}
addModel — 添加新模型
async function addModel() {
const id = document.getElementById('new-model-id').value.trim();
const name = document.getElementById('new-model-name').value.trim();
const url = document.getElementById('new-model-url').value.trim();
const key = document.getElementById('new-model-key').value.trim();
if (!id || !name || !url || !key) { showToast('请填写完整的模型信息'); return; }
try {
const res = await fetch(API_BASE + '/api/models/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: id, name, baseUrl: url, apiKey: key })
});
const data = await res.json();
if (data.success) {
showToast('模型已添加');
['new-model-id','new-model-name','new-model-url','new-model-key'].forEach(x => document.getElementById(x).value = '');
document.getElementById('add-model-details').open = false;
await loadModelsIntoPanel();
renderModelList();
} else {
showToast('添加失败: ' + (data.error || '未知错误'));
}
} catch (e) { showToast('网络错误'); }
}
editModel — 编辑模型
function editModel(modelId, currentName, currentUrl, isCustom) {
const newName = prompt('模型显示名称', currentName);
if (!newName) return;
const newUrl = prompt('API 地址', currentUrl);
if (!newUrl) return;
const updates = { name: newName, baseUrl: newUrl };
if (!isCustom) {
showToast('内置模型仅可在本地覆盖,下次打开页面时恢复默认。');
const idx = modelsCache.findIndex(m => m.id === modelId);
if (idx >= 0) { modelsCache[idx].name = newName; modelsCache[idx].url = newUrl; }
renderModelList();
return;
}
fetch(API_BASE + '/api/models/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customId: modelId, updates })
}).then(r => r.json()).then(data => {
if (data.success) {
showToast('模型已更新');
loadModelsIntoPanel().then(() => renderModelList());
} else {
showToast('更新失败: ' + (data.error || '未知错误'));
}
}).catch(() => showToast('网络错误'));
}
deleteModel — 删除模型
async function deleteModel(customId) {
if (!confirm('确定要删除该模型吗?')) return;
try {
const res = await fetch(API_BASE + '/api/models/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customId })
});
const data = await res.json();
if (data.success) {
showToast('模型已删除');
await loadModelsIntoPanel();
renderModelList();
} else {
showToast('删除失败: ' + (data.error || '未知错误'));
}
} catch (e) { showToast('网络错误'); }
}
openSettings — 打开面板时初始化
function openSettings() {
document.getElementById('api-modal').classList.add('open');
loadModelsIntoPanel();
renderModelList();
loadCurrentConfig();
loadToolsList();
loadSystemStatus();
}
CSS 样式
.field-row {
display: flex;
gap: 8px;
align-items: center;
}
.field-row input, .field-row select {
padding: 9px 12px;
border-radius: var(--radius-sm);
background: var(--bg-input);
border: 1px solid var(--border-base);
color: var(--text-strong);
font-size: 14px;
transition: border-color .2s;
outline: none;
}
.field-row input:focus, .field-row select:focus {
border-color: var(--brand);
box-shadow: 0 0 0 3px var(--brand-soft);
}
.btn {
padding: 9px 20px;
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 500;
transition: all .2s;
cursor: pointer;
border: none;
}
.btn-primary { background: var(--brand); color: #000; }
.btn-primary:hover { background: var(--brand-hover); }
.btn-secondary {
background: transparent;
color: var(--text-base);
border: 1px solid var(--border-base);
}
.btn-secondary:hover { background: rgba(255,255,255,.04); color: var(--text-strong); }
后端 API 端点
| 端点 | 方法 | 用途 |
| /api/models | GET | 获取所有模型(内置 + 自定义) |
| /api/models/add | POST | 添加自定义模型 |
| /api/models/update | POST | 更新自定义模型 |
| /api/models/delete | POST | 删除自定义模型 |
新旧对比
| 旧版 | 新版 |
| 两个独立卡片(模型配置 + 自定义库) | 一个统一卡片 |
| 模型选择和添加表单分离 | 模型列表内置单选,底部折叠添加表单 |
| 自定义模型显示在另一面板 | 所有模型在同一列表中,可编辑/删除 |
| 无单选按钮 | 每个模型前有单选按钮,选中即激活 |
| 切换后无自动同步 | 切换后自动同步 Base URL 到直调配置 |
注意事项
- 内置模型不可删除:删除按钮仅在
custom: true 时显示
- 内置模型配置覆盖:编辑内置模型时修改仅保存在前端缓存,页面刷新后恢复默认值
- Base URL 同步:选择模型后自动写入
settings-baseurl 和 localStorage
- 表单折叠:添加模型的表单默认隐藏,点击"+ 添加模型"展开