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))