288 lines
19 KiB
HTML
288 lines
19 KiB
HTML
<style>
|
||
.config-tab-bar { display:flex; gap:4px; border-bottom:2px solid #eee; flex-shrink:0; }
|
||
.config-tab { padding:8px 16px; font-size:13px; font-weight:600; color:#888; cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-2px; transition:all .2s; user-select:none; }
|
||
.config-tab:hover { color:#4a6cf7; }
|
||
.config-tab.active { color:#4a6cf7; border-bottom-color:#4a6cf7; }
|
||
.config-tab-content { display:none; }
|
||
.config-tab-content.active { display:block; }
|
||
</style>
|
||
|
||
<script>
|
||
let curConfigTab = 'chat';
|
||
|
||
function showConfigModal() {
|
||
curConfigTab = 'chat';
|
||
document.getElementById('configModal').classList.add('show');
|
||
switchConfigTab('chat');
|
||
loadConfigForm();
|
||
}
|
||
|
||
function hideConfigModal() {
|
||
document.getElementById('configModal').classList.remove('show');
|
||
}
|
||
|
||
function switchConfigTab(tab) {
|
||
curConfigTab = tab;
|
||
document.querySelectorAll('.config-tab').forEach(function(el) {
|
||
el.classList.toggle('active', el.dataset.tab === tab);
|
||
});
|
||
document.querySelectorAll('.config-tab-content').forEach(function(el) {
|
||
el.classList.toggle('active', el.id === 'cfg_tab_' + tab);
|
||
});
|
||
}
|
||
|
||
function switchVideoCategory() {
|
||
var sel = gid('cfg_videoModelCategory');
|
||
var refSections = gid('cfg_video_ref_sections');
|
||
if (!sel || !refSections) return;
|
||
refSections.style.display = sel.value === 'i2v' ? 'block' : 'none';
|
||
}
|
||
|
||
// ==================== 辅助函数 ====================
|
||
|
||
function gid(id) { return document.getElementById(id); }
|
||
function sv(id, val) { const el = gid(id); if (el) el.value = val; }
|
||
|
||
// ==================== 加载配置 ====================
|
||
|
||
async function loadConfigForm() {
|
||
try {
|
||
const resp = await fetch('/config/model');
|
||
const data = await resp.json();
|
||
if (data.code !== 0) throw new Error(data.message);
|
||
var cfg = data.data || {};
|
||
|
||
sv('cfg_chatModelName', cfg.chatModelName || '');
|
||
sv('cfg_chatApiKey', cfg.chatApiKey || '');
|
||
sv('cfg_chat_submit_url', cfg.chatBaseUrl || '');
|
||
if (cfg.maxTokens) sv('cfg_maxTokens', cfg.maxTokens);
|
||
if (cfg.temperature) sv('cfg_temperature', cfg.temperature);
|
||
|
||
sv('cfg_videoModelName', cfg.videoModelName || '');
|
||
sv('cfg_videoApiKey', cfg.videoApiKey || '');
|
||
sv('cfg_video_submit_url', cfg.videoBaseUrl || '');
|
||
sv('cfg_video_query_url', cfg.videoQueryUrl || '');
|
||
sv('cfg_videoModelCategory', cfg.videoModelCategory || 't2v');
|
||
if (cfg.minSingleDuration) sv('cfg_minSingleDuration', cfg.minSingleDuration);
|
||
if (cfg.maxSingleDuration) sv('cfg_maxSingleDuration', cfg.maxSingleDuration);
|
||
if (cfg.maxReferenceImages) sv('cfg_maxReferenceImages', cfg.maxReferenceImages);
|
||
if (cfg.maxRefVideoCount) sv('cfg_maxRefVideoCount', cfg.maxRefVideoCount);
|
||
if (cfg.maxRefVideoFileSize) sv('cfg_maxRefVideoFileSize', cfg.maxRefVideoFileSize);
|
||
if (cfg.maxRefVideoDuration) sv('cfg_maxRefVideoDuration', cfg.maxRefVideoDuration);
|
||
if (cfg.refVideoFormats) sv('cfg_refVideoFormats', cfg.refVideoFormats);
|
||
if (cfg.maxRefAudioCount) sv('cfg_maxRefAudioCount', cfg.maxRefAudioCount);
|
||
if (cfg.maxRefAudioFileSize) sv('cfg_maxRefAudioFileSize', cfg.maxRefAudioFileSize);
|
||
if (cfg.maxRefAudioDuration) sv('cfg_maxRefAudioDuration', cfg.maxRefAudioDuration);
|
||
if (cfg.refAudioFormats) sv('cfg_refAudioFormats', cfg.refAudioFormats);
|
||
if (cfg.maxRefImageFileSize) sv('cfg_maxRefImageFileSize', cfg.maxRefImageFileSize);
|
||
if (cfg.refImageFormats) sv('cfg_refImageFormats', cfg.refImageFormats);
|
||
switchVideoCategory();
|
||
} catch (err) { console.error('加载配置失败:', err); }
|
||
}
|
||
|
||
// ==================== 保存 ====================
|
||
|
||
async function saveConfig() {
|
||
var chatModelName = gid('cfg_chatModelName').value.trim();
|
||
var chatApiUrl = gid('cfg_chat_submit_url')?.value?.trim() || '';
|
||
var chatApiKey = gid('cfg_chatApiKey').value.trim();
|
||
|
||
if (!chatModelName) { alert('请输入对话模型名称'); return; }
|
||
if (!chatApiUrl) { alert('请输入对话模型请求地址'); return; }
|
||
if (!chatApiKey) { alert('请输入对话模型 API Key'); return; }
|
||
|
||
var maxTokens = parseInt(gid('cfg_maxTokens')?.value);
|
||
var temperature = parseFloat(gid('cfg_temperature')?.value);
|
||
if (!maxTokens || maxTokens < 1) { alert('请输入对话模型最大 Token 数'); return; }
|
||
if (temperature === undefined || isNaN(temperature) || temperature < 0) { alert('请输入对话模型 Temperature'); return; }
|
||
|
||
var videoModelName = gid('cfg_videoModelName').value.trim();
|
||
var videoApiUrl = gid('cfg_video_submit_url')?.value?.trim() || '';
|
||
var videoApiKey = gid('cfg_videoApiKey').value.trim();
|
||
var videoQueryUrl = gid('cfg_video_query_url')?.value?.trim() || '';
|
||
var videoModelCategory = gid('cfg_videoModelCategory')?.value || 't2v';
|
||
|
||
if (!videoModelName) { alert('请输入视频生成模型名称'); return; }
|
||
if (!videoApiUrl) { alert('请输入视频生成模型请求地址'); return; }
|
||
if (!videoApiKey) { alert('请输入视频生成模型 API Key'); return; }
|
||
if (!videoQueryUrl) { alert('请输入视频生成结果拉取地址'); return; }
|
||
|
||
// 图生图模式:校验参考字段
|
||
if (videoModelCategory === 'i2v') {
|
||
if (!parseInt(gid('cfg_maxRefVideoCount')?.value) > 0) { alert('图生图模式下,参考视频数量必须大于0'); return; }
|
||
if (!parseInt(gid('cfg_maxRefVideoFileSize')?.value) > 0) { alert('图生图模式下,单个参考视频最大大小必须填写'); return; }
|
||
if (!parseInt(gid('cfg_maxRefVideoDuration')?.value) > 0) { alert('图生图模式下,单个参考视频最大时长必须填写'); return; }
|
||
if (!gid('cfg_refVideoFormats')?.value?.trim()) { alert('图生图模式下,允许的视频格式不能为空'); return; }
|
||
if (!parseInt(gid('cfg_maxRefAudioCount')?.value) > 0) { alert('图生图模式下,参考音频数量必须大于0'); return; }
|
||
if (!parseInt(gid('cfg_maxRefAudioFileSize')?.value) > 0) { alert('图生图模式下,单个参考音频最大大小必须填写'); return; }
|
||
if (!parseInt(gid('cfg_maxRefAudioDuration')?.value) > 0) { alert('图生图模式下,单个参考音频最大时长必须填写'); return; }
|
||
if (!gid('cfg_refAudioFormats')?.value?.trim()) { alert('图生图模式下,允许的音频格式不能为空'); return; }
|
||
if (parseInt(gid('cfg_maxReferenceImages')?.value) < 2) { alert('图生图模式下,单次生成最大参考图数量至少为2'); return; }
|
||
if (!parseInt(gid('cfg_maxRefImageFileSize')?.value) > 0) { alert('图生图模式下,单个参考图片最大大小必须填写'); return; }
|
||
if (!gid('cfg_refImageFormats')?.value?.trim()) { alert('图生图模式下,允许的图片格式不能为空'); return; }
|
||
}
|
||
|
||
const btn = gid('cfg_saveBtn');
|
||
const loading = gid('cfg_loading');
|
||
const success = gid('cfg_successMsg');
|
||
btn.disabled = true; loading.classList.add('show'); success.classList.remove('show');
|
||
|
||
try {
|
||
const body = {
|
||
chatApiKey: chatApiKey,
|
||
chatBaseUrl: chatApiUrl,
|
||
chatModelName: chatModelName,
|
||
maxTokens: maxTokens,
|
||
temperature: temperature,
|
||
videoApiKey: videoApiKey,
|
||
videoBaseUrl: videoApiUrl,
|
||
videoModelName: videoModelName,
|
||
videoModelCategory: videoModelCategory,
|
||
videoQueryUrl: gid('cfg_video_query_url')?.value?.trim() || '',
|
||
minSingleDuration: parseInt(gid('cfg_minSingleDuration')?.value) || 2,
|
||
maxSingleDuration: parseInt(gid('cfg_maxSingleDuration')?.value) || 15,
|
||
maxReferenceImages: parseInt(gid('cfg_maxReferenceImages')?.value) || 9,
|
||
maxRefVideoCount: parseInt(gid('cfg_maxRefVideoCount')?.value) || 0,
|
||
maxRefVideoFileSize: parseInt(gid('cfg_maxRefVideoFileSize')?.value) || 0,
|
||
maxRefVideoDuration: parseInt(gid('cfg_maxRefVideoDuration')?.value) || 0,
|
||
refVideoFormats: gid('cfg_refVideoFormats')?.value || '',
|
||
maxRefAudioCount: parseInt(gid('cfg_maxRefAudioCount')?.value) || 0,
|
||
maxRefAudioFileSize: parseInt(gid('cfg_maxRefAudioFileSize')?.value) || 0,
|
||
maxRefAudioDuration: parseInt(gid('cfg_maxRefAudioDuration')?.value) || 0,
|
||
refAudioFormats: gid('cfg_refAudioFormats')?.value || '',
|
||
maxRefImageFileSize: parseInt(gid('cfg_maxRefImageFileSize')?.value) || 0,
|
||
refImageFormats: gid('cfg_refImageFormats')?.value || '',
|
||
};
|
||
const resp = await fetch('/config/model', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body)
|
||
});
|
||
const data = await resp.json();
|
||
if (data.code !== 0) throw new Error(data.message);
|
||
success.classList.add('show');
|
||
setTimeout(function() { success.classList.remove('show'); }, 3000);
|
||
} catch (err) {
|
||
alert('保存失败: ' + err.message);
|
||
} finally {
|
||
btn.disabled = false;
|
||
loading.classList.remove('show');
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<!-- 模型管理弹窗 -->
|
||
<div class="modal-overlay" id="configModal">
|
||
<div class="modal" style="width:80vw;height:80vh;max-width:1100px;padding:24px 28px;display:flex;flex-direction:column;">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-shrink:0;">
|
||
<h3 style="margin:0;">模型管理</h3>
|
||
<button class="btn btn-outline btn-sm" onclick="hideConfigModal()">关闭</button>
|
||
</div>
|
||
|
||
<!-- Tab 栏 -->
|
||
<div class="config-tab-bar" style="display:flex;">
|
||
<div class="config-tab active" data-tab="chat" onclick="switchConfigTab('chat')">对话模型</div>
|
||
<div class="config-tab" data-tab="video" onclick="switchConfigTab('video')">视频生成模型</div>
|
||
</div>
|
||
|
||
<div style="flex:1;overflow-y:auto;padding-right:8px;margin-top:16px;">
|
||
|
||
<!-- ========== 对话模型 ========== -->
|
||
<div class="config-tab-content active" id="cfg_tab_chat">
|
||
<div style="margin-bottom:16px;">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||
<span style="font-weight:600;font-size:15px;color:#1a1a2e;">基础配置</span>
|
||
<span style="font-size:11px;color:#fff;background:#4a6cf7;border-radius:3px;padding:1px 6px;">必填</span>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||
<div class="form-group" style="margin-bottom:0;"><label>模型名称 <span style="color:#e44;">*</span></label><input type="text" id="cfg_chatModelName" placeholder="qwen-max" style="width:100%;"></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>API Key <span style="color:#e44;">*</span></label><input type="password" id="cfg_chatApiKey" placeholder="sk-..." style="width:100%;"></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>请求地址 <span style="color:#e44;">*</span></label><input type="text" id="cfg_chat_submit_url" placeholder="https://..." style="width:100%;"></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>请求方式 <span style="color:#e44;">*</span></label><select id="cfg_chat_submit_method"><option value="POST">POST</option><option value="GET">GET</option></select></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||
<div class="form-group" style="margin-bottom:0;"><label>最大 Token 数 <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxTokens" value="4096" min="256" max="131072"><div class="help-text">超过长度自动截断</div></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>Temperature <span style="color:#e44;">*</span></label><input type="number" id="cfg_temperature" value="0.8" min="0" max="2" step="0.1"><div class="help-text">随机值(0-2)</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ========== 视频生成模型 ========== -->
|
||
<div class="config-tab-content" id="cfg_tab_video">
|
||
<div style="margin-bottom:16px;">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||
<span style="font-weight:600;font-size:15px;color:#1a1a2e;">基础配置</span>
|
||
<span style="font-size:11px;color:#fff;background:#4a6cf7;border-radius:3px;padding:1px 6px;">必填</span>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||
<div class="form-group" style="margin-bottom:0;"><label>模型名称 <span style="color:#e44;">*</span></label><input type="text" id="cfg_videoModelName" placeholder="wanx2.1-t2v-turbo" style="width:100%;"></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>API Key <span style="color:#e44;">*</span></label><input type="password" id="cfg_videoApiKey" placeholder="sk-..." style="width:100%;"></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>请求地址 <span style="color:#e44;">*</span></label><input type="text" id="cfg_video_submit_url" placeholder="https://..." style="width:100%;"></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>请求方式 <span style="color:#e44;">*</span></label><select id="cfg_video_submit_method"><option value="POST">POST</option><option value="GET">GET</option></select></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>结果拉取地址 <span style="color:#e44;">*</span></label><input type="text" id="cfg_video_query_url" placeholder="https://.../api/v1/tasks/{task_id}" style="width:100%;"><div class="help-text">用于查询视频生成任务状态和结果,{task_id} 会被自动替换</div></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>模型类型 <span style="color:#e44;">*</span></label><select id="cfg_videoModelCategory" onchange="switchVideoCategory()" style="width:100%;"><option value="t2v">文生图</option><option value="i2v">图生图</option></select></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||
<div class="form-group" style="margin-bottom:0;"><label>单次生成最小时长(秒)</label><input type="number" id="cfg_minSingleDuration" value="2" min="1" max="30"></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>单次生成最大时长(秒)</label><input type="number" id="cfg_maxSingleDuration" value="15" min="1" max="60"></div>
|
||
</div>
|
||
<div id="cfg_video_ref_sections" style="display:none;">
|
||
<!-- 参考视频约束 -->
|
||
<div style="margin-top:20px;padding-top:16px;border-top:1px solid #eee;">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||
<span style="font-weight:600;font-size:15px;color:#1a1a2e;">参考视频</span>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||
<div class="form-group" style="margin-bottom:0;"><label>参考视频数量 <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefVideoCount" value="0" min="0" max="50"><div class="help-text">0=不限制</div></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>单个视频最大大小(MB) <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefVideoFileSize" value="50" min="0" max="1024"></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>单个视频最大时长(秒) <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefVideoDuration" value="30" min="0" max="3600"></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>允许的视频格式 <span style="color:#e44;">*</span></label><input type="text" id="cfg_refVideoFormats" value="mp4,avi,mov,mkv,webm" placeholder="逗号分隔"><div class="help-text">逗号分隔,如 mp4,avi,mov</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 参考音频约束 -->
|
||
<div style="margin-top:20px;padding-top:16px;border-top:1px solid #eee;">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||
<span style="font-weight:600;font-size:15px;color:#1a1a2e;">参考音频</span>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||
<div class="form-group" style="margin-bottom:0;"><label>参考音频数量 <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefAudioCount" value="0" min="0" max="50"><div class="help-text">0=不限制</div></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>单个音频最大大小(MB) <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefAudioFileSize" value="10" min="0" max="1024"></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>单个音频最大时长(秒) <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefAudioDuration" value="30" min="0" max="3600"></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>允许的音频格式 <span style="color:#e44;">*</span></label><input type="text" id="cfg_refAudioFormats" value="wav,mp3,aac,flac,ogg" placeholder="逗号分隔"><div class="help-text">逗号分隔,如 wav,mp3,aac</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 参考图片约束 -->
|
||
<div style="margin-top:20px;padding-top:16px;border-top:1px solid #eee;">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||
<span style="font-weight:600;font-size:15px;color:#1a1a2e;">参考图片</span>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||
<div class="form-group" style="margin-bottom:0;grid-column:1/-1;">
|
||
<label>单次生成最大参考图数量 <span style="color:#e44;">*</span></label>
|
||
<input type="number" id="cfg_maxReferenceImages" value="9" min="1" max="50">
|
||
<div class="help-text">模型API限制的单次最大参考图片数(含首帧)。实际使用时首帧占1个,演员+场景+道具参考图共占用除此以外的数量。例如填写9,则每次生成最多使用8张参考图</div>
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>单个图片最大大小(MB) <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefImageFileSize" value="10" min="0" max="100"></div>
|
||
<div class="form-group" style="margin-bottom:0;"><label>允许的图片格式 <span style="color:#e44;">*</span></label><input type="text" id="cfg_refImageFormats" value="png,jpg,jpeg,webp,bmp" placeholder="逗号分隔"><div class="help-text">逗号分隔,如 png,jpg,jpeg,webp</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- 底部按钮 -->
|
||
<div style="flex-shrink:0;display:flex;gap:12px;justify-content:center;align-items:center;margin-top:16px;padding-top:14px;border-top:1px solid #eee;">
|
||
<div class="loading" id="cfg_loading" style="display:none;align-items:center;gap:6px;color:#888;font-size:13px;"><div class="spinner"></div><span>保存中...</span></div>
|
||
<button class="btn btn-primary" id="cfg_saveBtn" onclick="saveConfig()">保存配置</button>
|
||
<button class="btn btn-outline" onclick="hideConfigModal()">取消</button>
|
||
</div>
|
||
<div class="success-msg" id="cfg_successMsg">配置已保存</div>
|
||
</div>
|
||
</div>
|