From af5b975375fd70087fef7eb115a6a923c0cc406b Mon Sep 17 00:00:00 2001 From: pulsareonbot Date: Tue, 7 Jul 2026 12:26:45 +0800 Subject: [PATCH] =?UTF-8?q?fix(Web):=20=E8=A7=92=E8=89=B2=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=98=BE=E7=A4=BA=E6=89=80=E6=9C=89=E8=A7=92=E8=89=B2?= =?UTF-8?q?=20+=20=E5=8C=BA=E5=88=86=E8=A7=86=E9=A2=91/Live2D=20=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:切到 live2d 配置后角色列表只显示一个角色;角色卡片用当前配置的 character 信息渲染,而非各配置自己的。 1. routes.rs config_available_route 扩展返回结构 - configs 从字符串数组改为对象数组,每项含 filename + character 元信息 (name/character_type/render_type/live2d_model) - 读取每个配置文件提取 character 块 2. chat.js loadCharacters 用各配置自己的 character 元信息渲染卡片 - 显示角色名/类型徽章/渲染模式(video/live2d) - Live2D 角色额外显示模型路径 - 策略说明区分"视频播放+状态机"和"Canvas渲染+嘴部动效" 3. chat.js switchCharacter 改为 await loadConfig() 确保配置加载后再刷新 loadAvailableConfigs 兼容新旧返回结构 --- src/plugins/http/chat.js | 6 ++--- src/plugins/http/routes.rs | 50 ++++++++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/plugins/http/chat.js b/src/plugins/http/chat.js index 44b1ee6..cf03412 100644 --- a/src/plugins/http/chat.js +++ b/src/plugins/http/chat.js @@ -37,7 +37,7 @@ function stopAP(){if(wsReady)wsCmd({cmd:'ap_stop'});else api('POST','/api/wifi/a function loadBleStatus(){fetch('/api/ble/status').then(function(r){return r.json()}).then(function(d){var el=$('ble-status');if(d.running){el.textContent='运行中 / '+(d.device_name||'showen');el.className='val ok'}else{el.textContent='未就绪';el.className='val warn'}}).catch(function(){})} function startBLE(){api('POST','/api/ble/start',{device_name:$('ble-name').value||'showen'}).then(loadBleStatus)} function stopBLE(){api('POST','/api/ble/stop').then(loadBleStatus)} -function loadAvailableConfigs(){fetch('/api/config/available').then(function(r){return r.json()}).then(function(d){var sel=$('cfg-select');sel.innerHTML='';d.configs.forEach(function(f){var opt=document.createElement('option');opt.value=f;opt.textContent=f;if(f===d.active)opt.selected=true;sel.appendChild(opt)})}).catch(function(){})} +function loadAvailableConfigs(){fetch('/api/config/available').then(function(r){return r.json()}).then(function(d){var sel=$('cfg-select');sel.innerHTML='';(d.configs||[]).forEach(function(item){var f=typeof item==='string'?item:item.filename;var opt=document.createElement('option');opt.value=f;opt.textContent=f;if(f===d.active)opt.selected=true;sel.appendChild(opt)})}).catch(function(){})} function switchConfig(){var f=$('cfg-select').value;if(!f){toast('请选择配置',true);return}if(!confirm('确定切换到 '+f+' ?'))return;api('POST','/api/config/switch',{filename:f}).then(function(){loadConfig();loadAvailableConfigs()})} function loadConfig(){fetch('/api/config').then(function(r){return r.json()}).then(function(cfg){cachedConfig=cfg;$('cfg-editor').value=JSON.stringify(cfg,null,2);renderDisplay(cfg.display);applyCharStrategy(cfg.character||{})}).catch(function(){toast('加载配置失败',true)})} function formatConfig(){try{$('cfg-editor').value=JSON.stringify(JSON.parse($('cfg-editor').value),null,2)}catch(_){toast('JSON 格式错误',true)}} @@ -57,8 +57,8 @@ function triggerTalkState(talking){var scene=talking?(currentChar.talk_state||'t function showLive2dTalking(talking){live2dTalking=talking;if(!talking&&live2dModel&&live2dModel.internalModel&&live2dModel.internalModel.coreModel){live2dModel.internalModel.coreModel.setParameterValueById('ParamMouthOpenY',0)}} function addMsg(role,text){var box=$('chatBox');var div=document.createElement('div');div.className='msg '+role;var t=new Date().toLocaleTimeString('zh-CN',{hour12:false});div.innerHTML='
'+t+'
';div.querySelector('.bubble').textContent=text;box.appendChild(div);box.scrollTop=box.scrollHeight;return div} $('chatInput').addEventListener('keydown',function(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendChat()}}); -async function loadCharacters(){try{var res=await fetch('/api/config/available');var data=await res.json();var grid=$('charGrid');grid.innerHTML='';var cfgRes=await fetch('/api/config');var cfg=await cfgRes.json();var char=cfg.character||{};var strat=charStrategies[char.character_type||'pet']||charStrategies.pet;(data.configs||[]).forEach(function(c){var isActive=c===(data.active||'');var card=document.createElement('div');card.className='char-card'+(isActive?' active':'');var name=char.name||c.replace(/\.json$/,'').replace(/_/g,' ');card.innerHTML='
'+(strat.icon||'📦')+' '+name+'
'+strat.label+' '+(char.render_type||strat.render)+'
策略: 拟声/自然/元气 (依据角色)
'+(isActive?'使用中':'');card.onclick=function(){switchCharacter(c)};grid.appendChild(card)})}catch(e){$('charGrid').innerHTML='
加载失败
'}} -async function switchCharacter(filename){if(!confirm('切换到 '+filename+' ?'))return;try{var res=await fetch('/api/config/switch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({filename:filename})});var data=await res.json();if(data.status==='ok'||(data.message&&data.message.includes('成功'))){toast('已切换');loadConfig();loadCharacters()}else toast('失败: '+(data.message||'未知'),true)}catch(e){toast('失败: '+e.message,true)}} +async function loadCharacters(){try{var res=await fetch('/api/config/available');var data=await res.json();var grid=$('charGrid');grid.innerHTML='';(data.configs||[]).forEach(function(item){var filename=item.filename||item;var char=item.character||{};var ct=char.character_type||'pet';var strat=charStrategies[ct]||charStrategies.pet;var isActive=filename===(data.active||'');var name=char.name||filename.replace(/\.json$/,'').replace(/_/g,' ');var rt=char.render_type||strat.render;var modelInfo=char.live2d_model?'
模型: '+char.live2d_model+'':'';var card=document.createElement('div');card.className='char-card'+(isActive?' active':'');card.innerHTML='
'+(strat.icon||'📦')+' '+name+'
'+strat.label+' '+rt+'
'+(rt==='live2d'?'Canvas 渲染 + 嘴部动效':'视频播放 + 状态机')+modelInfo+'
'+(isActive?'使用中':'');card.onclick=function(){switchCharacter(filename)};grid.appendChild(card)})}catch(e){$('charGrid').innerHTML='
加载失败
'}} +async function switchCharacter(filename){if(!confirm('切换到 '+filename+' ?'))return;try{var res=await fetch('/api/config/switch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({filename:filename})});var data=await res.json();if(data.status==='ok'||(data.message&&data.message.includes('成功'))){toast('已切换');await loadConfig();loadCharacters()}else toast('失败: '+(data.message||'未知'),true)}catch(e){toast('失败: '+e.message,true)}} async function loadModels(){try{var res=await fetch('/api/models');var data=await res.json();var bar=$('modelBar');var u=Math.round((data.used_space||0)/1048576);var q=Math.round((data.quota||0)/1048576);bar.innerHTML='
配额:'+u+'/'+q+' MB
已下载:'+(data.models||[]).filter(function(m){return m.downloaded}).length+'/'+(data.models||[]).length+'
';var list=$('modelList');list.innerHTML='';var active=data.active||{};(data.models||[]).forEach(function(m){var item=document.createElement('div');item.className='model-item';var ia=active[m.kind]===m.id;var s=Math.round(m.size/1048576);var me=Math.round(m.memory_required/1048576);var a='';if(m.downloaded){if(ia)a='';else a=' '}else a='';item.innerHTML='
'+m.name+' '+m.kind.toUpperCase()+''+(m.downloaded?'已下载':'')+(ia?'活跃':'')+'
'+s+'MB | 需 '+me+'MB | '+m.recommended_tier+'
'+a+'
';list.appendChild(item)})}catch(e){$('modelList').innerHTML='
失败
'}} async function downloadModel(id){try{var res=await fetch('/api/models/download',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({model_id:id})});var data=await res.json();toast(data.message||'下载已启动');setTimeout(loadModels,3000)}catch(e){toast('失败',true)}} async function switchModel(id,kind){try{var res=await fetch('/api/models/switch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({model_id:id,kind:kind})});var data=await res.json();toast(data.message||'已切换');loadModels()}catch(e){toast('失败',true)}} diff --git a/src/plugins/http/routes.rs b/src/plugins/http/routes.rs index ddda3f1..aa86bdd 100644 --- a/src/plugins/http/routes.rs +++ b/src/plugins/http/routes.rs @@ -542,23 +542,57 @@ fn config_available_route( .and_then(|state: Arc| async move { let config = state.config(); let configs_dir = &config.source_dir; - let mut files: Vec = Vec::new(); - if let Ok(entries) = std::fs::read_dir(configs_dir) { - for entry in entries.flatten() { + // 每个配置文件提取 character 元信息,供前端渲染角色卡片 + let mut entries: Vec = Vec::new(); + if let Ok(read_entries) = std::fs::read_dir(configs_dir) { + for entry in read_entries.flatten() { let name = entry.file_name().to_string_lossy().into_owned(); - if name.ends_with(".json") { - files.push(name); + if !name.ends_with(".json") { + continue; } + let path = entry.path(); + let char_info = match std::fs::read_to_string(&path) { + Ok(content) => { + match serde_json::from_str::(&content) { + Ok(v) => { + let c = v.get("character").cloned().unwrap_or_default(); + serde_json::json!({ + "name": c.get("name").and_then(|n| n.as_str()).unwrap_or(&name).to_string(), + "character_type": c.get("character_type").and_then(|t| t.as_str()).unwrap_or("pet").to_string(), + "render_type": c.get("render_type").and_then(|r| r.as_str()).unwrap_or("video").to_string(), + "live2d_model": c.get("live2d_model").and_then(|m| m.as_str()).map(|s| s.to_string()), + }) + } + Err(_) => serde_json::json!({ + "name": name.clone(), + "character_type": "pet", + "render_type": "video", + "live2d_model": null, + }), + } + } + Err(_) => serde_json::json!({ + "name": name.clone(), + "character_type": "pet", + "render_type": "video", + "live2d_model": null, + }), + }; + entries.push(serde_json::json!({ + "filename": name, + "character": char_info, + })); } } - files.sort(); - // 标记当前活跃的配置 + entries.sort_by(|a, b| { + a["filename"].as_str().cmp(&b["filename"].as_str()) + }); let active = config.source_path .file_name() .map(|n| n.to_string_lossy().into_owned()) .unwrap_or_default(); let result = serde_json::json!({ - "configs": files, + "configs": entries, "active": active, }); Ok::<_, Infallible>(json_response(StatusCode::OK, &result))