@memtensor/memos-local-openclaw-plugin 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -512,6 +512,12 @@ input,textarea,select{font-family:inherit;font-size:inherit}
512
512
  .toggle-slider::before{content:'';position:absolute;height:14px;width:14px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.2s}
513
513
  .toggle-switch input:checked+.toggle-slider{background:var(--pri)}
514
514
  .toggle-switch input:checked+.toggle-slider::before{transform:translateX(16px)}
515
+ .test-conn-row{display:flex;align-items:center;gap:10px;margin-top:12px;padding-top:10px;border-top:1px dashed var(--border)}
516
+ .test-conn-row .btn{font-size:11px;padding:5px 14px;border:1px solid var(--border);border-radius:6px}
517
+ .test-result{font-size:12px;line-height:1.5;word-break:break-word}
518
+ .test-result.ok{color:#22c55e}
519
+ .test-result.fail{color:var(--rose)}
520
+ .test-result.loading{color:var(--text-muted)}
515
521
  .settings-actions{display:flex;gap:12px;justify-content:flex-end;align-items:center;margin-top:16px;padding-top:16px;border-top:1px solid var(--border)}
516
522
  .settings-actions .btn{min-width:110px;padding:10px 20px;font-size:13px}
517
523
  .settings-actions .btn-primary{background:rgba(99,102,241,.08);color:var(--pri);border:1px solid rgba(99,102,241,.25);font-weight:600}
@@ -939,9 +945,12 @@ input,textarea,select{font-family:inherit;font-size:inherit}
939
945
  <div class="settings-grid">
940
946
  <div class="settings-field">
941
947
  <label data-i18n="settings.provider">Provider</label>
942
- <select id="cfgEmbProvider">
948
+ <select id="cfgEmbProvider" onchange="onProviderChange('embedding')">
943
949
  <option value="openai_compatible">OpenAI Compatible</option>
944
950
  <option value="openai">OpenAI</option>
951
+ <option value="siliconflow">SiliconFlow (\u7845\u57FA\u6D41\u52A8)</option>
952
+ <option value="zhipu">Zhipu AI (\u667A\u8C31)</option>
953
+ <option value="bailian">Alibaba Bailian (\u767E\u70BC)</option>
945
954
  <option value="gemini">Gemini</option>
946
955
  <option value="azure_openai">Azure OpenAI</option>
947
956
  <option value="cohere">Cohere</option>
@@ -963,6 +972,10 @@ input,textarea,select{font-family:inherit;font-size:inherit}
963
972
  <input type="password" id="cfgEmbApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
964
973
  </div>
965
974
  </div>
975
+ <div class="test-conn-row">
976
+ <button class="btn btn-sm btn-ghost" onclick="testModel('embedding')" id="testEmbBtn" data-i18n="settings.test">Test Connection</button>
977
+ <span class="test-result" id="testEmbResult"></span>
978
+ </div>
966
979
  </div>
967
980
 
968
981
  <div class="settings-section">
@@ -970,9 +983,14 @@ input,textarea,select{font-family:inherit;font-size:inherit}
970
983
  <div class="settings-grid">
971
984
  <div class="settings-field">
972
985
  <label data-i18n="settings.provider">Provider</label>
973
- <select id="cfgSumProvider">
986
+ <select id="cfgSumProvider" onchange="onProviderChange('summarizer')">
974
987
  <option value="openai_compatible">OpenAI Compatible</option>
975
988
  <option value="openai">OpenAI</option>
989
+ <option value="siliconflow">SiliconFlow (\u7845\u57FA\u6D41\u52A8)</option>
990
+ <option value="zhipu">Zhipu AI (\u667A\u8C31)</option>
991
+ <option value="deepseek">DeepSeek</option>
992
+ <option value="bailian">Alibaba Bailian (\u767E\u70BC)</option>
993
+ <option value="moonshot">Moonshot (Kimi)</option>
976
994
  <option value="anthropic">Anthropic</option>
977
995
  <option value="gemini">Gemini</option>
978
996
  <option value="azure_openai">Azure OpenAI</option>
@@ -996,6 +1014,10 @@ input,textarea,select{font-family:inherit;font-size:inherit}
996
1014
  <input type="number" id="cfgSumTemp" step="0.1" min="0" max="2" placeholder="0">
997
1015
  </div>
998
1016
  </div>
1017
+ <div class="test-conn-row">
1018
+ <button class="btn btn-sm btn-ghost" onclick="testModel('summarizer')" id="testSumBtn" data-i18n="settings.test">Test Connection</button>
1019
+ <span class="test-result" id="testSumResult"></span>
1020
+ </div>
999
1021
  </div>
1000
1022
  </div>
1001
1023
 
@@ -1025,10 +1047,15 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1025
1047
  <div class="settings-grid">
1026
1048
  <div class="settings-field">
1027
1049
  <label data-i18n="settings.provider">Provider</label>
1028
- <select id="cfgSkillProvider">
1050
+ <select id="cfgSkillProvider" onchange="onProviderChange('skill')">
1029
1051
  <option value="">— <span data-i18n="settings.skill.usemain">Use main summarizer</span> —</option>
1030
1052
  <option value="openai_compatible">OpenAI Compatible</option>
1031
1053
  <option value="openai">OpenAI</option>
1054
+ <option value="siliconflow">SiliconFlow (\u7845\u57FA\u6D41\u52A8)</option>
1055
+ <option value="zhipu">Zhipu AI (\u667A\u8C31)</option>
1056
+ <option value="deepseek">DeepSeek</option>
1057
+ <option value="bailian">Alibaba Bailian (\u767E\u70BC)</option>
1058
+ <option value="moonshot">Moonshot (Kimi)</option>
1032
1059
  <option value="anthropic">Anthropic</option>
1033
1060
  <option value="gemini">Gemini</option>
1034
1061
  <option value="azure_openai">Azure OpenAI</option>
@@ -1048,6 +1075,10 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1048
1075
  <input type="password" id="cfgSkillApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
1049
1076
  </div>
1050
1077
  </div>
1078
+ <div class="test-conn-row">
1079
+ <button class="btn btn-sm btn-ghost" onclick="testModel('skill')" id="testSkillBtn" data-i18n="settings.test">Test Connection</button>
1080
+ <span class="test-result" id="testSkillResult"></span>
1081
+ </div>
1051
1082
  </div>
1052
1083
  </div>
1053
1084
 
@@ -1460,11 +1491,24 @@ const I18N={
1460
1491
  'settings.telemetry.hint':'Anonymous usage analytics to help improve the plugin. Only sends tool names, latencies, and version info. No memory content, queries, or personal data is ever sent.',
1461
1492
  'settings.viewerport':'Viewer Port',
1462
1493
  'settings.viewerport.hint':'Requires restart to take effect',
1494
+ 'settings.test':'Test Connection',
1495
+ 'settings.test.loading':'Testing...',
1496
+ 'settings.test.ok':'Connected',
1497
+ 'settings.test.fail':'Failed',
1463
1498
  'settings.save':'Save Settings',
1464
1499
  'settings.reset':'Reset',
1465
1500
  'settings.saved':'Saved',
1466
1501
  'settings.restart.hint':'Some changes require restarting the OpenClaw gateway to take effect.',
1467
1502
  'settings.save.fail':'Failed to save settings',
1503
+ 'settings.save.emb.required':'Embedding model is required. Please configure an embedding model before saving.',
1504
+ 'settings.save.emb.fail':'Embedding model test failed, cannot save',
1505
+ 'settings.save.sum.fail':'Summarizer model test failed, cannot save',
1506
+ 'settings.save.skill.fail':'Skill model test failed, cannot save',
1507
+ 'settings.save.sum.fallback':'Summarizer model is not configured — will use OpenClaw native model as fallback.',
1508
+ 'settings.save.skill.fallback':'Skill dedicated model is not configured — will use OpenClaw native model as fallback.',
1509
+ 'settings.save.fallback.model':'Fallback model: ',
1510
+ 'settings.save.fallback.none':'Not available (no OpenClaw native model found)',
1511
+ 'settings.save.fallback.confirm':'Continue to save?',
1468
1512
  'migrate.title':'Import OpenClaw Memory',
1469
1513
  'migrate.desc':'Migrate your existing OpenClaw built-in memories and conversation history into this plugin. The import process uses smart deduplication to avoid duplicates.',
1470
1514
  'migrate.modes.title':'Three ways to use:',
@@ -1753,11 +1797,24 @@ const I18N={
1753
1797
  'settings.telemetry.hint':'匿名使用统计,帮助改进插件。仅发送工具名称、响应时间和版本信息,不会发送任何记忆内容、搜索查询或个人数据。',
1754
1798
  'settings.viewerport':'Viewer 端口',
1755
1799
  'settings.viewerport.hint':'修改后需重启网关生效',
1800
+ 'settings.test':'测试连接',
1801
+ 'settings.test.loading':'测试中...',
1802
+ 'settings.test.ok':'连接成功',
1803
+ 'settings.test.fail':'连接失败',
1756
1804
  'settings.save':'保存设置',
1757
1805
  'settings.reset':'重置',
1758
1806
  'settings.saved':'已保存',
1759
1807
  'settings.restart.hint':'部分设置修改后需要重启 OpenClaw 网关才能生效。',
1760
1808
  'settings.save.fail':'保存设置失败',
1809
+ 'settings.save.emb.required':'嵌入模型为必填项,请先配置嵌入模型再保存。',
1810
+ 'settings.save.emb.fail':'嵌入模型测试失败,无法保存',
1811
+ 'settings.save.sum.fail':'摘要模型测试失败,无法保存',
1812
+ 'settings.save.skill.fail':'技能模型测试失败,无法保存',
1813
+ 'settings.save.sum.fallback':'摘要模型未配置 — 将使用 OpenClaw 原生模型作为降级方案。',
1814
+ 'settings.save.skill.fallback':'技能专用模型未配置 — 将使用 OpenClaw 原生模型作为降级方案。',
1815
+ 'settings.save.fallback.model':'降级模型:',
1816
+ 'settings.save.fallback.none':'不可用(未检测到 OpenClaw 原生模型)',
1817
+ 'settings.save.fallback.confirm':'是否继续保存?',
1761
1818
  'migrate.title':'导入 OpenClaw 记忆',
1762
1819
  'migrate.desc':'将 OpenClaw 内置的记忆数据和对话历史迁移到本插件中。导入过程使用智能去重,避免重复导入。',
1763
1820
  'migrate.modes.title':'三种使用方式:',
@@ -2285,7 +2342,6 @@ async function loadTasks(){
2285
2342
  '</div>'+
2286
2343
  '<div class="card-actions" onclick="event.stopPropagation()">'+
2287
2344
  '<button class="btn btn-sm btn-ghost" onclick="openTaskDetail(\\''+task.id+'\\')">'+t('card.expand')+'</button>'+
2288
- '<button class="btn btn-sm btn-ghost" onclick="editTaskInline(\\''+task.id+'\\')">'+t('card.edit')+'</button>'+
2289
2345
  (task.status==='completed'&&(!task.skillStatus||task.skillStatus==='not_generated'||task.skillStatus==='skipped')?'<button class="btn btn-sm btn-ghost" onclick="retrySkillGen(\\''+task.id+'\\')">'+t('task.retrySkill.short')+'</button>':'')+
2290
2346
  '<button class="btn btn-sm btn-ghost" style="color:var(--accent)" onclick="deleteTask(\\''+task.id+'\\')">'+t('task.delete')+'</button>'+
2291
2347
  '</div>'+
@@ -2458,32 +2514,6 @@ async function deleteTask(taskId){
2458
2514
  }catch(e){ alert(t('task.delete.error')+e.message); }
2459
2515
  }
2460
2516
 
2461
- async function editTaskInline(){
2462
- if(!_currentTaskData) return;
2463
- var task=_currentTaskData;
2464
- var titleEl=document.getElementById('taskDetailTitle');
2465
- var summaryEl=document.getElementById('taskDetailSummary');
2466
- var actionsEl=document.getElementById('taskDetailActions');
2467
-
2468
- titleEl.innerHTML='<input id="editTaskTitle" class="filter-input" style="width:100%;font-size:16px;font-weight:600" value="'+esc(task.title||'')+'"/>';
2469
- summaryEl.innerHTML='<textarea id="editTaskSummary" class="filter-input" style="width:100%;min-height:80px;font-size:13px;resize:vertical">'+esc(task.summary||'')+'</textarea>';
2470
- actionsEl.innerHTML=
2471
- '<button class="btn btn-primary" onclick="saveTaskEdit()" style="font-size:12px">'+t('task.save')+'</button>'+
2472
- '<button class="btn btn-ghost" onclick="openTaskDetail(\\''+esc(task.id)+'\\')" style="font-size:12px">'+t('task.cancel')+'</button>';
2473
- }
2474
-
2475
- async function saveTaskEdit(){
2476
- if(!_currentTaskId) return;
2477
- var title=document.getElementById('editTaskTitle').value.trim();
2478
- var summary=document.getElementById('editTaskSummary').value.trim();
2479
- try{
2480
- const r=await fetch('/api/task/'+_currentTaskId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:title,summary:summary})});
2481
- const d=await r.json();
2482
- if(!r.ok) throw new Error(d.error||'unknown');
2483
- openTaskDetail(_currentTaskId);
2484
- loadTasks();
2485
- }catch(e){ alert(t('task.save.error')+e.message); }
2486
- }
2487
2517
 
2488
2518
  /* ─── Skills View Logic ─── */
2489
2519
  let skillsStatusFilter='';
@@ -2544,7 +2574,6 @@ async function loadSkills(){
2544
2574
  '</div>'+
2545
2575
  '<div class="card-actions" onclick="event.stopPropagation()">'+
2546
2576
  '<button class="btn btn-sm btn-ghost" onclick="openSkillDetail(\\''+skill.id+'\\')">'+t('card.expand')+'</button>'+
2547
- '<button class="btn btn-sm btn-ghost" onclick="editSkillInline(\\''+skill.id+'\\')">'+t('card.edit')+'</button>'+
2548
2577
  (skill.visibility==='public'?'<button class="btn btn-sm btn-ghost" onclick="toggleSkillPublic(\\''+skill.id+'\\',false)">\\u{1F512} '+t('skills.setPrivate')+'</button>':'<button class="btn btn-sm btn-ghost" onclick="toggleSkillPublic(\\''+skill.id+'\\',true)">\\u{1F310} '+t('skills.setPublic')+'</button>')+
2549
2578
  '<button class="btn btn-sm btn-ghost" style="color:var(--accent)" onclick="deleteSkill(\\''+skill.id+'\\')">'+t('skill.delete')+'</button>'+
2550
2579
  '</div>'+
@@ -2691,11 +2720,11 @@ async function toggleSkillVisibility(){
2691
2720
  const newVis=btn.dataset.vis==='public'?'private':'public';
2692
2721
  try{
2693
2722
  const r=await fetch('/api/skill/'+currentSkillId+'/visibility',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({visibility:newVis})});
2694
- if(!r.ok) throw new Error('Failed: '+r.status);
2723
+ if(!r.ok){var errBody='';try{var ej=await r.json();errBody=ej.error||JSON.stringify(ej);}catch(x){errBody=await r.text();}throw new Error(r.status+': '+errBody);}
2695
2724
  openSkillDetail(currentSkillId);
2696
2725
  loadSkills();
2697
2726
  }catch(e){
2698
- alert('Error: '+e.message);
2727
+ toast('Error: '+e.message,'error');
2699
2728
  }
2700
2729
  }
2701
2730
 
@@ -2703,7 +2732,7 @@ async function toggleSkillPublic(id,setPublic){
2703
2732
  const newVis=setPublic?'public':'private';
2704
2733
  try{
2705
2734
  const r=await fetch('/api/skill/'+id+'/visibility',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({visibility:newVis})});
2706
- if(!r.ok) throw new Error('Failed: '+r.status);
2735
+ if(!r.ok){var errBody='';try{var ej=await r.json();errBody=ej.error||JSON.stringify(ej);}catch(x){errBody=await r.text();}throw new Error(r.status+': '+errBody);}
2707
2736
  toast(setPublic?t('toast.setPublic'):t('toast.setPrivate'),'success');
2708
2737
  loadSkills();
2709
2738
  }catch(e){
@@ -2751,7 +2780,37 @@ async function loadConfig(){
2751
2780
  }
2752
2781
  }
2753
2782
 
2783
+ var _providerDefaults={
2784
+ siliconflow:{endpoint:'https://api.siliconflow.cn/v1',embModel:'BAAI/bge-m3',chatModel:'Qwen/Qwen2.5-7B-Instruct'},
2785
+ openai:{endpoint:'https://api.openai.com/v1',embModel:'text-embedding-3-small',chatModel:'gpt-4o-mini'},
2786
+ anthropic:{endpoint:'https://api.anthropic.com/v1/messages',chatModel:'claude-3-haiku-20240307'},
2787
+ cohere:{endpoint:'https://api.cohere.com/v2',embModel:'embed-english-v3.0'},
2788
+ mistral:{endpoint:'https://api.mistral.ai/v1',embModel:'mistral-embed'},
2789
+ voyage:{endpoint:'https://api.voyageai.com/v1',embModel:'voyage-3'},
2790
+ gemini:{endpoint:'',embModel:'text-embedding-004',chatModel:'gemini-2.0-flash'},
2791
+ zhipu:{endpoint:'https://open.bigmodel.cn/api/paas/v4',embModel:'embedding-3',chatModel:'glm-4-flash'},
2792
+ deepseek:{endpoint:'https://api.deepseek.com/v1',chatModel:'deepseek-chat'},
2793
+ bailian:{endpoint:'https://dashscope.aliyuncs.com/compatible-mode/v1',embModel:'text-embedding-v3',chatModel:'qwen-max'},
2794
+ moonshot:{endpoint:'https://api.moonshot.cn/v1',chatModel:'moonshot-v1-8k'}
2795
+ };
2796
+ function onProviderChange(section){
2797
+ var map={embedding:['cfgEmbEndpoint','cfgEmbModel','emb'],summarizer:['cfgSumEndpoint','cfgSumModel','chat'],skill:['cfgSkillEndpoint','cfgSkillModel','chat']};
2798
+ var m=map[section];if(!m)return;
2799
+ var sel=document.getElementById(section==='embedding'?'cfgEmbProvider':section==='summarizer'?'cfgSumProvider':'cfgSkillProvider');
2800
+ var pv=sel.value;
2801
+ var def=_providerDefaults[pv];
2802
+ if(!def)return;
2803
+ var epEl=document.getElementById(m[0]);
2804
+ var mdEl=document.getElementById(m[1]);
2805
+ if(def.endpoint&&!epEl.value.trim()) epEl.value=def.endpoint;
2806
+ if(m[2]==='emb'&&def.embModel&&!mdEl.value.trim()) mdEl.value=def.embModel;
2807
+ if(m[2]==='chat'&&def.chatModel&&!mdEl.value.trim()) mdEl.value=def.chatModel;
2808
+ }
2809
+
2754
2810
  async function saveConfig(){
2811
+ var saveBtn=document.querySelector('.settings-actions .btn-primary');
2812
+ saveBtn.disabled=true;saveBtn.textContent=t('settings.test.loading');
2813
+
2755
2814
  const cfg={};
2756
2815
  const embP=document.getElementById('cfgEmbProvider').value;
2757
2816
  if(embP){
@@ -2761,11 +2820,15 @@ async function saveConfig(){
2761
2820
  const k=document.getElementById('cfgEmbApiKey').value.trim();if(k) cfg.embedding.apiKey=k;
2762
2821
  }
2763
2822
  const sumP=document.getElementById('cfgSumProvider').value;
2764
- if(sumP){
2823
+ const sumModel=document.getElementById('cfgSumModel').value.trim();
2824
+ const sumEndpoint=document.getElementById('cfgSumEndpoint').value.trim();
2825
+ const sumApiKey=document.getElementById('cfgSumApiKey').value.trim();
2826
+ var hasSumConfig=!!(sumModel||sumEndpoint||sumApiKey);
2827
+ if(hasSumConfig&&sumP){
2765
2828
  cfg.summarizer={provider:sumP};
2766
- const v=document.getElementById('cfgSumModel').value.trim();if(v) cfg.summarizer.model=v;
2767
- const e=document.getElementById('cfgSumEndpoint').value.trim();if(e) cfg.summarizer.endpoint=e;
2768
- const k=document.getElementById('cfgSumApiKey').value.trim();if(k) cfg.summarizer.apiKey=k;
2829
+ if(sumModel) cfg.summarizer.model=sumModel;
2830
+ if(sumEndpoint) cfg.summarizer.endpoint=sumEndpoint;
2831
+ if(sumApiKey) cfg.summarizer.apiKey=sumApiKey;
2769
2832
  const tp=document.getElementById('cfgSumTemp').value.trim();if(tp!=='') cfg.summarizer.temperature=Number(tp);
2770
2833
  }
2771
2834
  cfg.skillEvolution={
@@ -2776,29 +2839,118 @@ async function saveConfig(){
2776
2839
  const mk=document.getElementById('cfgSkillMinChunks').value.trim();if(mk) cfg.skillEvolution.minChunksForEval=Number(mk);
2777
2840
 
2778
2841
  const skP=document.getElementById('cfgSkillProvider').value;
2779
- if(skP){
2842
+ const skModel=document.getElementById('cfgSkillModel').value.trim();
2843
+ const skEndpoint=document.getElementById('cfgSkillEndpoint').value.trim();
2844
+ const skApiKey=document.getElementById('cfgSkillApiKey').value.trim();
2845
+ var hasSkillConfig=!!(skP&&(skModel||skEndpoint||skApiKey));
2846
+ if(hasSkillConfig){
2780
2847
  cfg.skillEvolution.summarizer={provider:skP};
2781
- const sv=document.getElementById('cfgSkillModel').value.trim();if(sv) cfg.skillEvolution.summarizer.model=sv;
2782
- const se=document.getElementById('cfgSkillEndpoint').value.trim();if(se) cfg.skillEvolution.summarizer.endpoint=se;
2783
- const sk=document.getElementById('cfgSkillApiKey').value.trim();if(sk) cfg.skillEvolution.summarizer.apiKey=sk;
2848
+ if(skModel) cfg.skillEvolution.summarizer.model=skModel;
2849
+ if(skEndpoint) cfg.skillEvolution.summarizer.endpoint=skEndpoint;
2850
+ if(skApiKey) cfg.skillEvolution.summarizer.apiKey=skApiKey;
2784
2851
  }
2785
2852
 
2786
2853
  const vp=document.getElementById('cfgViewerPort').value.trim();
2787
2854
  if(vp) cfg.viewerPort=Number(vp);
2855
+ cfg.telemetry={enabled:document.getElementById('cfgTelemetryEnabled').checked};
2788
2856
 
2789
- cfg.telemetry={
2790
- enabled:document.getElementById('cfgTelemetryEnabled').checked
2791
- };
2857
+ function done(){saveBtn.disabled=false;saveBtn.textContent=t('settings.save');}
2858
+
2859
+ // 1) Embedding model is required
2860
+ if(!embP||embP===''){done();toast(t('settings.save.emb.required'),'error');return;}
2792
2861
 
2862
+ // 2) Test embedding
2863
+ try{
2864
+ var er=await fetch('/api/test-model',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:'embedding',provider:cfg.embedding.provider,model:cfg.embedding.model||'',endpoint:cfg.embedding.endpoint||'',apiKey:cfg.embedding.apiKey||''})});
2865
+ var ed=await er.json();
2866
+ if(!ed.ok){done();toast(t('settings.save.emb.fail')+': '+ed.error,'error');document.getElementById('testEmbResult').className='test-result fail';document.getElementById('testEmbResult').innerHTML='\\u274C '+ed.error;return;}
2867
+ document.getElementById('testEmbResult').className='test-result ok';document.getElementById('testEmbResult').innerHTML='\\u2705 '+t('settings.test.ok');
2868
+ }catch(e){done();toast(t('settings.save.emb.fail')+': '+e.message,'error');return;}
2869
+
2870
+ // 3) Test summarizer if user filled it
2871
+ if(hasSumConfig&&cfg.summarizer){
2872
+ try{
2873
+ var sr=await fetch('/api/test-model',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:'summarizer',provider:cfg.summarizer.provider,model:cfg.summarizer.model||'',endpoint:cfg.summarizer.endpoint||'',apiKey:cfg.summarizer.apiKey||''})});
2874
+ var sd=await sr.json();
2875
+ if(!sd.ok){done();toast(t('settings.save.sum.fail')+': '+sd.error,'error');document.getElementById('testSumResult').className='test-result fail';document.getElementById('testSumResult').innerHTML='\\u274C '+sd.error;return;}
2876
+ document.getElementById('testSumResult').className='test-result ok';document.getElementById('testSumResult').innerHTML='\\u2705 '+t('settings.test.ok');
2877
+ }catch(e){done();toast(t('settings.save.sum.fail')+': '+e.message,'error');return;}
2878
+ }
2879
+
2880
+ // 4) Test skill model if user filled it
2881
+ if(hasSkillConfig&&cfg.skillEvolution.summarizer){
2882
+ try{
2883
+ var kr=await fetch('/api/test-model',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:'summarizer',provider:cfg.skillEvolution.summarizer.provider,model:cfg.skillEvolution.summarizer.model||'',endpoint:cfg.skillEvolution.summarizer.endpoint||'',apiKey:cfg.skillEvolution.summarizer.apiKey||''})});
2884
+ var kd=await kr.json();
2885
+ if(!kd.ok){done();toast(t('settings.save.skill.fail')+': '+kd.error,'error');document.getElementById('testSkillResult').className='test-result fail';document.getElementById('testSkillResult').innerHTML='\\u274C '+kd.error;return;}
2886
+ document.getElementById('testSkillResult').className='test-result ok';document.getElementById('testSkillResult').innerHTML='\\u2705 '+t('settings.test.ok');
2887
+ }catch(e){done();toast(t('settings.save.skill.fail')+': '+e.message,'error');return;}
2888
+ }
2889
+
2890
+ // 5) If summarizer or skill model not configured, check OpenClaw fallback and confirm
2891
+ if(!hasSumConfig||!hasSkillConfig){
2892
+ try{
2893
+ var fr=await fetch('/api/fallback-model');
2894
+ var fb=await fr.json();
2895
+ var msgs=[];
2896
+ if(!hasSumConfig){msgs.push(t('settings.save.sum.fallback'));}
2897
+ if(!hasSkillConfig){msgs.push(t('settings.save.skill.fallback'));}
2898
+ var fbInfo=fb.available?(fb.model+' ('+fb.baseUrl+')'):t('settings.save.fallback.none');
2899
+ var confirmMsg=msgs.join('\\n')+'\\n\\n'+t('settings.save.fallback.model')+fbInfo+'\\n\\n'+t('settings.save.fallback.confirm');
2900
+ if(!confirm(confirmMsg)){done();return;}
2901
+ }catch(e){}
2902
+ }
2903
+
2904
+ // 6) All tests passed, save
2793
2905
  try{
2794
2906
  const r=await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(cfg)});
2795
2907
  if(!r.ok) throw new Error(await r.text());
2796
2908
  const el=document.getElementById('settingsSaved');
2797
2909
  el.classList.add('show');
2798
2910
  setTimeout(()=>el.classList.remove('show'),2500);
2911
+ toast(t('settings.saved'),'success');
2799
2912
  }catch(e){
2800
- showToast(t('settings.save.fail')+': '+e.message,'error');
2913
+ toast(t('settings.save.fail')+': '+e.message,'error');
2914
+ }finally{done();}
2915
+ }
2916
+
2917
+ async function testModel(type){
2918
+ var ids={embedding:['Emb','cfgEmbProvider','cfgEmbModel','cfgEmbEndpoint','cfgEmbApiKey'],summarizer:['Sum','cfgSumProvider','cfgSumModel','cfgSumEndpoint','cfgSumApiKey'],skill:['Skill','cfgSkillProvider','cfgSkillModel','cfgSkillEndpoint','cfgSkillApiKey']};
2919
+ var c=ids[type];if(!c)return;
2920
+ var resultEl=document.getElementById('test'+c[0]+'Result');
2921
+ var btn=document.getElementById('test'+c[0]+'Btn');
2922
+ var provider=document.getElementById(c[1]).value;
2923
+ var model=document.getElementById(c[2]).value.trim();
2924
+ var endpoint=document.getElementById(c[3]).value.trim();
2925
+ var apiKey=document.getElementById(c[4]).value.trim();
2926
+ if(!provider||(provider!=='local'&&!model)){
2927
+ resultEl.className='test-result fail';
2928
+ resultEl.innerHTML='\\u274C '+t('settings.test.fail')+'<div style="margin-top:4px;font-size:11px;color:var(--text-muted)">Provider and Model are required</div>';
2929
+ return;
2801
2930
  }
2931
+ if(provider!=='local'&&!apiKey){
2932
+ resultEl.className='test-result fail';
2933
+ resultEl.innerHTML='\\u274C '+t('settings.test.fail')+'<div style="margin-top:4px;font-size:11px;color:var(--text-muted)">API Key is required</div>';
2934
+ return;
2935
+ }
2936
+ resultEl.className='test-result loading';resultEl.textContent=t('settings.test.loading');
2937
+ btn.disabled=true;
2938
+ try{
2939
+ var body={type:type,provider:provider,model:model,endpoint:endpoint,apiKey:apiKey};
2940
+ var r=await fetch('/api/test-model',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
2941
+ var d=await r.json();
2942
+ if(d.ok){
2943
+ resultEl.className='test-result ok';
2944
+ resultEl.innerHTML='\\u2705 '+t('settings.test.ok')+'<div style="margin-top:4px;font-size:11px;color:var(--text-muted)">'+esc(d.detail||'')+'</div>';
2945
+ }else{
2946
+ var errMsg=d.error||'Unknown error';
2947
+ resultEl.className='test-result fail';
2948
+ resultEl.innerHTML='\\u274C '+t('settings.test.fail')+'<div style="margin-top:6px;font-size:11px;padding:8px 10px;background:rgba(239,68,68,.06);border:1px solid rgba(239,68,68,.15);border-radius:6px;white-space:pre-wrap;word-break:break-all;max-height:120px;overflow-y:auto;font-family:SF Mono,Monaco,Consolas,monospace">'+esc(errMsg)+'</div>';
2949
+ }
2950
+ }catch(e){
2951
+ resultEl.className='test-result fail';
2952
+ resultEl.innerHTML='\\u274C '+t('settings.test.fail')+'<div style="margin-top:6px;font-size:11px;padding:8px 10px;background:rgba(239,68,68,.06);border:1px solid rgba(239,68,68,.15);border-radius:6px;white-space:pre-wrap;word-break:break-all">'+esc(e.message)+'</div>';
2953
+ }finally{btn.disabled=false;}
2802
2954
  }
2803
2955
 
2804
2956
  function renderSkillMarkdown(md){
@@ -2844,28 +2996,6 @@ async function deleteSkill(skillId){
2844
2996
  }catch(e){ alert(t('skill.delete.error')+e.message); }
2845
2997
  }
2846
2998
 
2847
- function editSkillInline(){
2848
- var skill=window._currentSkillData;
2849
- if(!skill) return;
2850
- var descEl=document.getElementById('skillDetailDesc');
2851
- var actionsEl=document.getElementById('skillDetailActions');
2852
- descEl.innerHTML='<textarea id="editSkillDesc" class="filter-input" style="width:100%;min-height:60px;font-size:13px;resize:vertical">'+esc(skill.description||'')+'</textarea>';
2853
- actionsEl.innerHTML=
2854
- '<button class="btn btn-primary" onclick="saveSkillEdit()" style="font-size:12px">'+t('skill.save')+'</button>'+
2855
- '<button class="btn btn-ghost" onclick="openSkillDetail(\\''+esc(skill.id)+'\\')" style="font-size:12px">'+t('skill.cancel')+'</button>';
2856
- }
2857
-
2858
- async function saveSkillEdit(){
2859
- if(!currentSkillId) return;
2860
- var desc=document.getElementById('editSkillDesc').value.trim();
2861
- try{
2862
- const r=await fetch('/api/skill/'+currentSkillId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({description:desc})});
2863
- const d=await r.json();
2864
- if(!r.ok) throw new Error(d.error||'unknown');
2865
- openSkillDetail(currentSkillId);
2866
- loadSkills();
2867
- }catch(e){ alert(t('skill.save.error')+e.message); }
2868
- }
2869
2999
 
2870
3000
  function formatDuration(ms){
2871
3001
  const s=Math.floor(ms/1000);