探查结果:
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模型选择器
• 原生
好的,用户这次提供了探查结果,明确了当前的技术现状。用户的核心诉求很清晰:在WebUI的模型选择器里,给每个自定义模型加上编辑按钮,点击后能弹出窗口修改模型信息。
嗯,我需要先理解当前的技术基础。探查结果告诉我,后端已经有了完整的用户模型管理函数,包括updateUserModel,这很好,意味着后端逻辑基本就绪。前端方面,当前的模型选择器是一个原生的<select>下拉框,这确实是个限制,因为原生下拉框无法在选项里嵌入按钮。上一轮已经实现了添加模型的功能,包括API和前端弹窗。
用户可能没有说出来的深层需求是:希望模型管理功能完整闭环,添加、编辑、删除都能在界面上直观操作,而不需要回到CLI。这能提升WebUI作为独立管理界面的可用性。
那么,我的思考需要围绕如何克服<select>的限制来展开。显然,直接改造<select>不可行,必须改变模型选择器的实现方式。最稳妥的方案是把它换成用<div>模拟的自定义下拉菜单。这样,每个模型选项就变成了一个独立的元素,可以轻松在里面放编辑和删除按钮。
在构思具体方案时,我需要调用之前了解到的项目架构信息:后端是Bun,前端是单文件HTML。所以,我的方案必须基于现有的技术栈,不引入额外框架。我得设计一个纯原生JavaScript实现的自定义下拉组件,样式上要能匹配现有的Apple风格设计系统。
具体的实现逻辑应该是这样的:自定义下拉菜单的每个模型项,如果带有custom标识(说明是用户添加的),就显示编辑(✏️)和删除(🗑️)按钮。点击编辑时,需要弹出一个编辑表单,这个表单可以复用或类似上一轮添加模型时的弹窗结构,但需要预填当前模型的信息。表单提交后,调用一个新的后端API POST /api/models/update,成功后刷新列表。
后端方面,因为updateUserModel函数已经存在,我只需要在src/web.ts里加一个路由来处理这个更新请求,调用现有函数,把新的配置信息传递进去就可以了。这改动很小,风险也低。
最后,我得确保整个方案是原子级的,方便用户或助手直接执行。这包括具体的代码修改位置、完整的代码片段、以及验证步骤。同时要覆盖回滚方案,确保安全。
当前架构清晰,后端 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> 替换为自定义下拉组件<select id="modelSelector"> 及其 <option> 动态生成逻辑loadModels() 中操作 <select> 的代码段(保留数据加载部分)。<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>
<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; }
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 =
<span class="model-name">${model.custom ? '⭐ ' : ''}${model.name}</span>
<span class="model-provider">${model.provider || ''}</span>
${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';
}
});
<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>
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
所有修改均尊重现有架构(每模型独立 .enc,updateUserModel 函数已就绪),仅新增 API 路由和前端组件,无需改动存储逻辑。