1. 修改了一些bug
2. 做了一些调整,比如启动脚本,支持了tenmsorboard
This commit is contained in:
@@ -5,6 +5,18 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>模型管理 / 远光软件微调平台</title>
|
||||
<script src="../lib/tailwindcss/tailwind.js"></script>
|
||||
<script>
|
||||
// 禁用 Tailwind 开发模式警告
|
||||
if (typeof console !== 'undefined' && console.warn) {
|
||||
const originalWarn = console.warn;
|
||||
console.warn = function(...args) {
|
||||
if (args[0] && args[0].includes && args[0].includes('cdn.tailwindcss.com')) {
|
||||
return;
|
||||
}
|
||||
originalWarn.apply(console, args);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.sidebar-section-title {
|
||||
@@ -95,7 +107,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-item-wrapper">
|
||||
<a href="#" data-page="my-models" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<a href="model-manage.html" data-page="my-models" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-database w-5 text-center"></i>
|
||||
<span class="ml-2">我的模型</span>
|
||||
</a>
|
||||
@@ -248,6 +260,7 @@
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用途</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">模型来源</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">描述</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">模型路径</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">创建时间</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||
</tr>
|
||||
@@ -290,16 +303,18 @@
|
||||
<p class="text-gray-500">暂无已训练模型</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// API 基础地址 - 使用 config.yaml 中的 app.port (7861)
|
||||
const getApiBase = () => {
|
||||
const protocol = window.location.protocol;
|
||||
const hostname = window.location.hostname;
|
||||
return `${protocol}//${hostname}:7861/api`;
|
||||
};
|
||||
const API_BASE = getApiBase();
|
||||
// 使用 IIFE 避免全局变量污染
|
||||
(function() {
|
||||
// API 基础地址 - 优先使用 main.html 中定义的全局变量
|
||||
const getApiBase = () => {
|
||||
const protocol = window.location.protocol;
|
||||
const hostname = window.location.hostname;
|
||||
return `${protocol}//${hostname}:7861/api`;
|
||||
};
|
||||
const API_BASE = typeof window.API_BASE !== 'undefined' ? window.API_BASE : getApiBase();
|
||||
|
||||
let allModels = [];
|
||||
let trainedModels = [];
|
||||
@@ -339,19 +354,94 @@
|
||||
// 加载模型数据
|
||||
async function loadModels() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/model-manage`);
|
||||
const result = await response.json();
|
||||
// 并行加载数据库模型和已训练模型
|
||||
const [dbResponse, trainedResponse] = await Promise.all([
|
||||
fetch(`${API_BASE}/model-manage`),
|
||||
fetch(`${API_BASE}/model-manage/trained-models`)
|
||||
]);
|
||||
|
||||
if (result.code === 0) {
|
||||
allModels = result.data || [];
|
||||
renderModels();
|
||||
const dbResult = await dbResponse.json();
|
||||
const trainedResult = await trainedResponse.json();
|
||||
|
||||
console.log('[DEBUG] 数据库模型数量:', dbResult.data?.length || 0);
|
||||
console.log('[DEBUG] 已训练模型API响应:', trainedResult);
|
||||
|
||||
// 数据库模型
|
||||
let dbModels = [];
|
||||
if (dbResult.code === 0) {
|
||||
dbModels = dbResult.data || [];
|
||||
}
|
||||
|
||||
// 已训练模型 - 转换为统一格式
|
||||
trainedModels = [];
|
||||
if (trainedResult.code === 0) {
|
||||
const trainedData = trainedResult.data?.models || [];
|
||||
console.log('[DEBUG] 已训练模型数据:', trainedData);
|
||||
trainedData.forEach(model => {
|
||||
// 每个训练方法作为一个模型条目
|
||||
if (model.train_methods && model.train_methods.length > 0) {
|
||||
model.train_methods.forEach(method => {
|
||||
trainedModels.push({
|
||||
id: `trained_${model.name}_${method.name}`.replace(/[^a-zA-Z0-9]/g, '_'),
|
||||
name: `${model.name} (${method.name})`,
|
||||
type: 'LLM',
|
||||
purpose: 'inference',
|
||||
model_source: 'local',
|
||||
path: method.path,
|
||||
description: `基于 ${model.name} 的${getMethodDisplayName(method.name)}训练模型`,
|
||||
create_time: new Date().toISOString(),
|
||||
isTrained: true,
|
||||
baseModel: model.name,
|
||||
trainMethod: method.name
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// 没有训练方法的也添加为模型
|
||||
trainedModels.push({
|
||||
id: `trained_${model.name}`.replace(/[^a-zA-Z0-9]/g, '_'),
|
||||
name: model.name,
|
||||
type: 'LLM',
|
||||
purpose: 'inference',
|
||||
model_source: 'local',
|
||||
path: model.path,
|
||||
description: '已训练模型',
|
||||
create_time: new Date().toISOString(),
|
||||
isTrained: true,
|
||||
baseModel: model.name,
|
||||
trainMethod: ''
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 合并所有模型
|
||||
allModels = [...dbModels, ...trainedModels];
|
||||
console.log('[DEBUG] 数据库模型:', dbModels.length);
|
||||
console.log('[DEBUG] 已训练模型:', trainedModels.length);
|
||||
console.log('[DEBUG] 合并后的模型总数:', allModels.length);
|
||||
renderModels();
|
||||
} catch (error) {
|
||||
console.error('加载模型失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载已训练模型数据
|
||||
// 获取训练方法显示名称
|
||||
function getMethodDisplayName(method) {
|
||||
const methodMap = {
|
||||
'lora': 'LoRA',
|
||||
'qlora': 'QLoRA',
|
||||
'full': '全量微调',
|
||||
'prefix': 'Prefix Tuning',
|
||||
'adapter': 'Adapter',
|
||||
'lora_plus': 'LoRA+',
|
||||
'peft': 'PEFT',
|
||||
'adalora': 'AdaLoRA',
|
||||
'longlora': 'LongLoRA'
|
||||
};
|
||||
return methodMap[method] || method;
|
||||
}
|
||||
|
||||
// 加载已训练模型数据(仅用于Trained Tab)
|
||||
async function loadTrainedModels() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/model-manage/trained-models`);
|
||||
@@ -375,7 +465,8 @@
|
||||
|
||||
// 渲染模型列表
|
||||
function renderModels() {
|
||||
const searchValue = document.getElementById('searchInput').value.toLowerCase();
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchValue = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
let filteredModels = allModels;
|
||||
|
||||
// 按 Tab 筛选
|
||||
@@ -394,13 +485,15 @@
|
||||
const tbody = document.getElementById('modelsBody');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
|
||||
if (!tbody) return;
|
||||
|
||||
if (filteredModels.length === 0) {
|
||||
tbody.innerHTML = '';
|
||||
emptyState.classList.remove('hidden');
|
||||
if (emptyState) emptyState.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
if (emptyState) emptyState.classList.add('hidden');
|
||||
|
||||
tbody.innerHTML = filteredModels.map(item => {
|
||||
// 模型类型
|
||||
@@ -429,10 +522,38 @@
|
||||
};
|
||||
const sourceDisplay = sourceMap[item.model_source] || item.model_source || '-';
|
||||
|
||||
// 判断是否是已训练模型
|
||||
const isTrained = item.isTrained === true;
|
||||
const trainedBadge = isTrained ? '<span class="ml-2 px-2 py-0.5 text-xs font-medium rounded bg-green-100 text-green-700">已训练</span>' : '';
|
||||
|
||||
// 操作按钮
|
||||
let actionButtons = '';
|
||||
if (isTrained) {
|
||||
// 已训练模型:显示查看和复制路径按钮
|
||||
actionButtons = `
|
||||
<button onclick="viewTrainedModel('${(item.path || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'")}')" class="text-primary hover:text-primary/80 mr-3">
|
||||
<i class="fa fa-folder-open"></i> 查看
|
||||
</button>
|
||||
<button onclick="copyModelPath('${(item.path || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'")}')" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fa fa-copy"></i> 复制路径
|
||||
</button>
|
||||
`;
|
||||
} else {
|
||||
// 普通模型:编辑和删除
|
||||
actionButtons = `
|
||||
<button onclick="editModel(${item.id})" class="text-primary hover:text-primary/80 mr-3">
|
||||
<i class="fa fa-edit"></i> 编辑
|
||||
</button>
|
||||
<button onclick="deleteModel(${item.id})" class="text-red-500 hover:text-red-600">
|
||||
<i class="fa fa-trash"></i> 删除
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-gray-50">
|
||||
<tr class="hover:bg-gray-50 ${isTrained ? 'bg-green-50/30' : ''}">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">${item.name || '-'}</div>
|
||||
<div class="text-sm font-medium text-gray-900">${item.name || '-'}${trainedBadge}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 py-1 text-xs font-medium rounded bg-blue-100 text-blue-700">${typeDisplay}</span>
|
||||
@@ -444,18 +565,16 @@
|
||||
<span class="px-2 py-1 text-xs font-medium rounded bg-gray-100 text-gray-700">${sourceDisplay}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-500 max-w-xs truncate">${item.description || '-'}</div>
|
||||
<div class="text-sm text-gray-500 max-w-xs truncate" title="${item.description || ''}">${item.description || '-'}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-500 max-w-xs truncate ${isTrained ? 'font-mono text-green-600' : ''}" title="${item.path || ''}">${item.path ? (isTrained ? item.path.split('/').pop() : item.path) : '-'}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
${item.create_time ? new Date(item.create_time).toLocaleString('zh-CN') : '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<button onclick="editModel(${item.id})" class="text-primary hover:text-primary/80 mr-3">
|
||||
<i class="fa fa-edit"></i> 编辑
|
||||
</button>
|
||||
<button onclick="deleteModel(${item.id})" class="text-red-500 hover:text-red-600">
|
||||
<i class="fa fa-trash"></i> 删除
|
||||
</button>
|
||||
${actionButtons}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
@@ -523,7 +642,57 @@
|
||||
|
||||
// 查看已训练模型
|
||||
function viewTrainedModel(path) {
|
||||
alert(`模型路径: ${path}\n\n您可以从此路径加载模型进行推理或评测。`);
|
||||
// 显示模型路径详情
|
||||
const content = `
|
||||
<div class="text-left">
|
||||
<p class="mb-3 text-gray-600">模型路径:</p>
|
||||
<div class="bg-gray-100 p-3 rounded text-sm font-mono break-all mb-3">${path}</div>
|
||||
<p class="text-sm text-gray-500">您可以复制此路径用于推理或评测。</p>
|
||||
</div>
|
||||
`;
|
||||
// 使用简单的提示框
|
||||
showModal('模型详情', content);
|
||||
}
|
||||
|
||||
// 复制模型路径
|
||||
function copyModelPath(path) {
|
||||
navigator.clipboard.writeText(path).then(() => {
|
||||
showToast('模型路径已复制到剪贴板');
|
||||
}).catch(() => {
|
||||
// 降级方案
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = path;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
showToast('模型路径已复制到剪贴板');
|
||||
});
|
||||
}
|
||||
|
||||
// 显示弹窗
|
||||
function showModal(title, content) {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
||||
modal.innerHTML = `
|
||||
<div class="bg-white rounded-lg p-6 max-w-lg w-full mx-4">
|
||||
<h3 class="text-lg font-medium mb-4">${title}</h3>
|
||||
<div class="mb-4">${content}</div>
|
||||
<div class="flex justify-end">
|
||||
<button onclick="this.closest('.fixed').remove()" class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// 显示提示
|
||||
function showToast(message) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded shadow-lg z-50';
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 2000);
|
||||
}
|
||||
|
||||
// 编辑模型
|
||||
@@ -554,6 +723,7 @@
|
||||
|
||||
// 页面加载时初始化
|
||||
loadModels();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user