👤 用户

探查结果: 1. 存储方式 • 每模型独立 .enc 文件 • 目录:~/.cmdcode/workspaces/default/models/ • 当前为空(上次测试的已删除) • 无单文件 user_models.enc 2. 可用函数签名 函数 签名 createUserModel (username, config) → UserModelConfig loadUserModel (username, modelId) → UserModelConfig updateUserModel (username, modelId, updates: Partial<...>) → UserModelConfig deleteUserModel (username, modelId) → boolean 3. WebUI模型选择器 • 原生 内无法嵌入编辑/删除按钮 需要我继续实现编辑和删除的前端入口吗?

🪐 DeepSeek 思考过程

好的,用户这次提供了探查结果,明确了当前的技术现状。用户的核心诉求很清晰:在WebUI的模型选择器里,给每个自定义模型加上编辑按钮,点击后能弹出窗口修改模型信息。

嗯,我需要先理解当前的技术基础。探查结果告诉我,后端已经有了完整的用户模型管理函数,包括updateUserModel,这很好,意味着后端逻辑基本就绪。前端方面,当前的模型选择器是一个原生的<select>下拉框,这确实是个限制,因为原生下拉框无法在选项里嵌入按钮。上一轮已经实现了添加模型的功能,包括API和前端弹窗。

用户可能没有说出来的深层需求是:希望模型管理功能完整闭环,添加、编辑、删除都能在界面上直观操作,而不需要回到CLI。这能提升WebUI作为独立管理界面的可用性。

那么,我的思考需要围绕如何克服<select>的限制来展开。显然,直接改造<select>不可行,必须改变模型选择器的实现方式。最稳妥的方案是把它换成用<div>模拟的自定义下拉菜单。这样,每个模型选项就变成了一个独立的元素,可以轻松在里面放编辑和删除按钮。

在构思具体方案时,我需要调用之前了解到的项目架构信息:后端是Bun,前端是单文件HTML。所以,我的方案必须基于现有的技术栈,不引入额外框架。我得设计一个纯原生JavaScript实现的自定义下拉组件,样式上要能匹配现有的Apple风格设计系统。

具体的实现逻辑应该是这样的:自定义下拉菜单的每个模型项,如果带有custom标识(说明是用户添加的),就显示编辑(✏️)和删除(🗑️)按钮。点击编辑时,需要弹出一个编辑表单,这个表单可以复用或类似上一轮添加模型时的弹窗结构,但需要预填当前模型的信息。表单提交后,调用一个新的后端API POST /api/models/update,成功后刷新列表。

后端方面,因为updateUserModel函数已经存在,我只需要在src/web.ts里加一个路由来处理这个更新请求,调用现有函数,把新的配置信息传递进去就可以了。这改动很小,风险也低。

最后,我得确保整个方案是原子级的,方便用户或助手直接执行。这包括具体的代码修改位置、完整的代码片段、以及验证步骤。同时要覆盖回滚方案,确保安全。

🤖 DeepSeek 助手

✅ 执行确认

当前架构清晰,后端 updateUserModel 已就绪,只需补齐 API 路由和前端入口。由于 <select> 无法内嵌按钮,需要将模型选择器升级为自定义下拉组件,每个模型项支持编辑/删除操作。


🔧 原子级实施方案

分三步走:后端补充更新接口 → 前端改造选择器为自定义列表 → 绑定编辑/删除事件。


一、后端:新增 POST /api/models/update 路由

文件:src/web.ts 位置:紧接 POST /api/models/delete 路由之后

// 更新用户自定义模型
if (url.pathname === '/api/models/update' && req.method === 'POST') {
    try {
        const body = await req.json();
        const { modelId, updates } = body;
        if (!modelId || !updates) {
            return new Response(JSON.stringify({ error: 'modelId 和 updates 为必填项' }), { status: 400 });
        }
        // 安全检查:禁止修改内置模型
        const builtin = getAvailableModels().find(m => m.id === modelId);
        if (builtin && !builtin.custom) {
            return new Response(JSON.stringify({ error: '不允许修改内置模型' }), { status: 403 });
        }
        const updated = updateUserModel('administrator', modelId, updates);
        return new Response(JSON.stringify({ success: true, model: updated }), {
            headers: { 'Content-Type': 'application/json' }
        });
    } catch (e) {
        return new Response(JSON.stringify({ error: e.message }), { status: 500 });
    }
}

导入检查:确保 src/web.ts 顶部已导入 updateUserModel

grep -n "updateUserModel" /home/administrator/CmdCode-V0.5/src/web.ts
若无,在文件顶部添加:
import { listUserModels, createUserModel, updateUserModel, deleteUserModel } from './user-models';

二、前端:将 <select> 替换为自定义下拉组件

A. 删除原有 <select id="modelSelector"> 及其 <option> 动态生成逻辑

定位并注释掉 loadModels() 中操作 <select> 的代码段(保留数据加载部分)。

B. 在 <select> 原位置插入自定义下拉容器

<!-- 替换原来的 <select id="modelSelector"> -->
<div class="custom-select" id="model-select-container">
    <div class="select-selected" id="model-select-trigger">
        <span id="selected-model-name">MiniMax-M2.7</span>
        <span class="arrow">▼</span>
    </div>
    <div class="select-items" id="model-select-dropdown" style="display:none;">
        <!-- 动态填充模型列表 -->
    </div>
</div>

C. 在 <style> 末尾追加样式(约 60 行)

.custom-select { position: relative; display: inline-block; min-width: 200px; }
.select-selected { 
    padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; 
    cursor: pointer; display: flex; justify-content: space-between; align-items: center;
    background: var(--bg-primary); color: var(--text-primary);
}
.select-selected:hover { border-color: var(--accent); }
.select-items { 
    position: absolute; top: 100%; left: 0; right: 0; z-index: 100;
    background: var(--bg-primary); border: 1px solid var(--border); 
    border-radius: 8px; margin-top: 4px; max-height: 320px; overflow-y: auto;
    box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.model-option {
    padding: 10px 14px; cursor: pointer; display: flex; justify-content: space-between; align-items: center;
    border-bottom: 1px solid var(--border-light); transition: background 0.15s;
}
.model-option:hover { background: var(--bg-secondary); }
.model-option:last-child { border-bottom: none; }
.model-option .model-name { flex: 1; font-weight: 500; }
.model-option .model-provider { font-size: 0.8em; color: var(--text-secondary); margin-left: 8px; }
.model-option .model-actions { display: flex; gap: 4px; margin-left: 8px; }
.model-option .model-actions button {
    background: none; border: 1px solid var(--border); border-radius: 4px; 
    cursor: pointer; padding: 2px 6px; font-size: 12px; transition: background 0.15s;
}
.model-option .model-actions button:hover { background: var(--bg-tertiary); }
.model-option .model-actions .edit-btn:hover { color: var(--accent); }
.model-option .model-actions .delete-btn:hover { color: #e74c3c; }

D. 重写 loadModels() 函数

async function loadModels() {
    const res = await fetch(apiBase + '/api/models');
    const data = await res.json();
    const dropdown = document.getElementById('model-select-dropdown');
    dropdown.innerHTML = '';

data.models.forEach(model => { const item = document.createElement('div'); item.className = 'model-option'; item.innerHTML = &lt;span class="model-name"&gt;${model.custom ? '⭐ ' : ''}${model.name}&lt;/span&gt; &lt;span class="model-provider"&gt;${model.provider || ''}&lt;/span&gt; ${model.custom ? <span class="model-actions"> <button class="edit-btn" data-id="${model.id}" title="编辑">✏️</button> <button class="delete-btn" data-id="${model.id}" title="删除">🗑️</button> </span> : ''} ;

// 点击模型名切换 item.querySelector('.model-name')?.addEventListener('click', () => { document.getElementById('selected-model-name').textContent = model.name; localStorage.setItem('selectedModel', model.id); dropdown.style.display = 'none'; });

// 编辑按钮 item.querySelector('.edit-btn')?.addEventListener('click', (e) => { e.stopPropagation(); showEditModelForm(model); });

// 删除按钮 item.querySelector('.delete-btn')?.addEventListener('click', async (e) => { e.stopPropagation(); if (!confirm(确认删除模型 "${model.name}"?)) return; await fetch(apiBase + '/api/models/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId: model.id }) }); loadModels(); });

dropdown.appendChild(item); });

// 恢复已选模型 const savedId = localStorage.getItem('selectedModel'); if (savedId) { const saved = data.models.find(m => m.id === savedId); if (saved) document.getElementById('selected-model-name').textContent = saved.name; } }

// 下拉开关 document.getElementById('model-select-trigger').addEventListener('click', () => { const dropdown = document.getElementById('model-select-dropdown'); dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; }); document.addEventListener('click', (e) => { if (!e.target.closest('#model-select-container')) { document.getElementById('model-select-dropdown').style.display = 'none'; } });

E. 新增编辑弹窗 HTML(紧接添加模型弹窗之后)

<div id="edit-model-modal" class="modal-sheet" style="display: none;">
  <div class="modal-content">
    <h3>编辑模型</h3>
    <input type="hidden" id="edit-model-id" />
    <input type="text" id="edit-model-name" placeholder="显示名称" />
    <input type="text" id="edit-model-provider" placeholder="厂商" />
    <input type="text" id="edit-model-url" placeholder="接口地址" />
    <input type="password" id="edit-model-key" placeholder="API Key(留空不修改)" />
    <div style="display:flex; gap: 8px; margin-top: 12px;">
      <button id="confirm-edit-model" style="flex:1; background: var(--accent); color: white;">保存</button>
      <button id="cancel-edit-model" style="flex:1;">取消</button>
    </div>
  </div>
</div>

F. 编辑逻辑函数

function showEditModelForm(model) {
    document.getElementById('edit-model-id').value = model.id;
    document.getElementById('edit-model-name').value = model.name;
    document.getElementById('edit-model-provider').value = model.provider || '';
    document.getElementById('edit-model-url').value = model.url || '';
    document.getElementById('edit-model-key').value = '';
    document.getElementById('edit-model-modal').style.display = 'flex';
}

document.getElementById('cancel-edit-model').addEventListener('click', () => { document.getElementById('edit-model-modal').style.display = 'none'; });

document.getElementById('confirm-edit-model').addEventListener('click', async () => { const modelId = document.getElementById('edit-model-id').value; const name = document.getElementById('edit-model-name').value.trim(); const provider = document.getElementById('edit-model-provider').value.trim(); const url = document.getElementById('edit-model-url').value.trim(); const key = document.getElementById('edit-model-key').value.trim();

if (!name) return alert('模型名称为必填'); const updates = {}; if (name) updates.name = name; if (provider) updates.provider = provider; if (url) updates.baseUrl = url; if (key) updates.apiKey = key;

const res = await fetch(apiBase + '/api/models/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId, updates }) }); const data = await res.json(); if (data.success) { document.getElementById('edit-model-modal').style.display = 'none'; loadModels(); showToast('模型已更新'); } else { alert('更新失败: ' + data.error); } });


三、验证与回滚

# 1. 重启后端
pkill -f "bun run src/cli.ts" 2>/dev/null
cd /home/administrator/CmdCode-V0.5 && bun run src/cli.ts &

# 2. 打开 WebUI,点击模型选择器 → 出现自定义下拉列表 # 3. 添加一个测试模型 → 确认出现 ⭐ 标记 + 编辑/删除按钮 # 4. 点击 ✏️ → 修改名称 → 保存 → 列表刷新 # 5. 点击 🗑️ → 确认 → 模型消失

回滚:如有异常,恢复备份文件

cp webui/index.html.bak.cssdedup webui/index.html
cp src/web.ts.bak.$(date +%Y%m%d) src/web.ts

所有修改均尊重现有架构(每模型独立 .encupdateUserModel 函数已就绪),仅新增 API 路由和前端组件,无需改动存储逻辑。