👤 用户
设置页面的「模型与 API 配置 (/model, /set)」和「自定义模型库 (/model add)」两个区块完全可以统一合并: - 面板统一显示所有可调用的模型列表 - 每个模型后面加上「配置」「删除」两个按钮 - 配置按钮里可以完全修改模型型号、显示名称、接入链接地址、API Key 所有参数 - 每个大模型前面有一个选择点,选择后点保存就执行切换 - 模型选择板块下还有一个添加大模型的按钮 请给出完整的合并方案。
🤖 DeepSeek 助手

用一张卡片替代原来的两个独立区块,统一管理模型选择、添加、配置和删除。

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/modelsGET获取所有模型(内置 + 自定义)
/api/models/addPOST添加自定义模型
/api/models/updatePOST更新自定义模型
/api/models/deletePOST删除自定义模型

新旧对比

旧版新版
两个独立卡片(模型配置 + 自定义库)一个统一卡片
模型选择和添加表单分离模型列表内置单选,底部折叠添加表单
自定义模型显示在另一面板所有模型在同一列表中,可编辑/删除
无单选按钮每个模型前有单选按钮,选中即激活
切换后无自动同步切换后自动同步 Base URL 到直调配置

注意事项

  • 内置模型不可删除:删除按钮仅在 custom: true 时显示
  • 内置模型配置覆盖:编辑内置模型时修改仅保存在前端缓存,页面刷新后恢复默认值
  • Base URL 同步:选择模型后自动写入 settings-baseurllocalStorage
  • 表单折叠:添加模型的表单默认隐藏,点击"+ 添加模型"展开