失败重试次数
@@ -1293,6 +1303,30 @@ PAGE_SETTINGS = """
async function jget(u){const r=await fetch(u);return r.json()}
function esc(s){const d=document.createElement('div');d.textContent=s==null?'':String(s);return d.innerHTML}
function showMsg(id,t,ok){const m=document.getElementById(id);m.textContent=t;m.className='msg '+(ok?'ok':'err');}
+// ---- 数值设置:改动即生效(失焦/回车自动保存,无需点按钮) ----
+let lastVals={};
+function valChanged(inputId,key){const v=document.getElementById(inputId).value;if(lastVals[key]===v)return false;lastVals[key]=v;return true;}
+function flashSaved(id,ok,msg){
+ const el=document.getElementById(id);
+ el.textContent=ok?'✓ 已生效':('✗ '+(msg||'保存失败'));
+ el.className='saved-tip '+(ok?'ok':'err');
+ if(ok)setTimeout(()=>{if(el.textContent==='✓ 已生效')el.textContent='';},2000);
+}
+async function saveSetting(key,inputId,tipId,field){
+ if(!valChanged(inputId,key))return; // 没改动不重复保存
+ const v=parseInt(document.getElementById(inputId).value,10);
+ if(isNaN(v)){flashSaved(tipId,false,'请输入有效数字');return;}
+ let r;
+ try{
+ r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},
+ body:JSON.stringify({[field]:v})})).json();
+ }catch(e){flashSaved(tipId,false,'网络错误');return;}
+ await refresh(); // 刷新后非法值会自动回滚成服务器值
+ flashSaved(tipId,!!r.ok,r.msg);
+}
+async function saveInterval(){return saveSetting('interval','syncInterval','intervalSaved','interval');}
+async function saveWorkers(){return saveSetting('workers','syncWorkers','workersSaved','max_workers');}
+async function saveRetries(){return saveSetting('retries','syncRetries','retriesSaved','max_retries');}
async function refresh(){
try{
const s=await jget('api/settings');
@@ -1304,13 +1338,14 @@ async function refresh(){
document.getElementById('swAuto').checked=!!s.auto_create;
const iv=document.getElementById('syncInterval');
iv.value=s.interval||10;iv.min=s.min_interval||5;iv.max=s.max_interval||86400;
- document.getElementById('intervalTip').textContent='每隔 '+fmtInterval(s.interval||10)+' 检查一次 Gitea 是否有新提交('+(s.min_interval||5)+'~'+(s.max_interval||86400)+' 秒),改完立即生效';
+ document.getElementById('intervalTip').textContent='每隔 '+fmtInterval(s.interval||10)+' 检查一次 Gitea 是否有新提交('+(s.min_interval||5)+'~'+(s.max_interval||86400)+' 秒),改动即生效';
const wk=document.getElementById('syncWorkers');
wk.value=s.max_workers||3;wk.min=s.min_workers||1;wk.max=s.max_workers_limit||8;
document.getElementById('workersTip').textContent='每轮最多 '+(s.max_workers||3)+' 个项目同时克隆/推送(1~'+(s.max_workers_limit||8)+'),项目多时能显著加快;网络或 NAS 负载高就调小';
const rt=document.getElementById('syncRetries');
rt.value=(s.max_retries==null?5:s.max_retries);rt.min=0;rt.max=20;
document.getElementById('retriesTip').textContent='同步失败后每隔 '+(s.retry_interval||60)+' 秒自动重试失败的仓库,最多 '+(s.max_retries==null?5:s.max_retries)+' 次(0 = 不重试);与同步间隔无关,专治网络抖动';
+ lastVals={interval:iv.value,workers:wk.value,retries:rt.value};
const cur=(s.accounts||[]).find(a=>a.id===s.active);
// 当前账号无 Token 时,顶部提示为什么不能自动建库、怎么解锁
document.getElementById('accBanner').innerHTML=(cur&&!cur.has_token)
@@ -1400,33 +1435,6 @@ function fmtInterval(s){
if(s>=60)return Math.round(s/6)/10+' 分钟';
return s+' 秒';
}
-async function saveInterval(){
- const v=parseInt(document.getElementById('syncInterval').value,10);
- const b=document.getElementById('btnInterval');b.disabled=true;b.textContent='⏳ 保存中…';
- try{
- const r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({interval:v})})).json();
- showMsg('msgCfg',r.msg||JSON.stringify(r),!!r.ok);
- }catch(e){showMsg('msgCfg','请求失败: '+e,false)}
- finally{b.disabled=false;b.textContent='保存间隔';refresh();}
-}
-async function saveWorkers(){
- const v=parseInt(document.getElementById('syncWorkers').value,10);
- const b=document.getElementById('btnWorkers');b.disabled=true;b.textContent='⏳ 保存中…';
- try{
- const r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({max_workers:v})})).json();
- showMsg('msgCfg',r.msg||JSON.stringify(r),!!r.ok);
- }catch(e){showMsg('msgCfg','请求失败: '+e,false)}
- finally{b.disabled=false;b.textContent='保存并发';refresh();}
-}
-async function saveRetries(){
- const v=parseInt(document.getElementById('syncRetries').value,10);
- const b=document.getElementById('btnRetries');b.disabled=true;b.textContent='⏳ 保存中…';
- try{
- const r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({max_retries:v})})).json();
- showMsg('msgCfg',r.msg||JSON.stringify(r),!!r.ok);
- }catch(e){showMsg('msgCfg','请求失败: '+e,false)}
- finally{b.disabled=false;b.textContent='保存次数';refresh();}
-}
refresh();
"""
diff --git a/GiteaHubSync/manifest b/GiteaHubSync/manifest
index 1c31bde..b16e4e7 100644
--- a/GiteaHubSync/manifest
+++ b/GiteaHubSync/manifest
@@ -1,5 +1,5 @@
appname = GiteaHubSync
-version = 1.4.0
+version = 1.4.1
display_name = Gitea→GitHub 同步助手
desc = 每次推送到飞牛 Gitea 的代码,10 秒内自动镜像到 GitHub 的 7 个私有仓库。内置控制面板:自动同步开关、立即同步、仓库状态、同步日志,一键打开 Gitea。数据全部保存在 NAS 本地。
platform = all
diff --git a/README.md b/README.md
index fc49ba2..0ac47a0 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,8 @@ NAS 重启自动恢复。控制面板带自动同步开关、立即同步、仓
- **同步间隔可调**:设置页可改检查间隔(5 秒 ~ 1 天,默认 10 秒),改完立即生效
- **多项目并行同步**:每轮最多 N 个项目同时克隆/推送(默认 3,可调 1~8);
在状态页点单个仓库的「同步」也不再被整轮同步阻塞(同一仓库有防重入)
+- **设置改动即生效**:同步间隔 / 并发数 / 重试次数等数值设置改完自动保存
+ (失焦或回车即生效,无需点按钮),非法值自动回滚并提示
- **失败自动重试**:同步失败后每隔 1 分钟自动重试失败的仓库,最多 N 次
(默认 5,可调 0~20,0 = 不重试);重试与同步间隔无关,专治网络抖动,
重试中的仓库在状态页显示「重试中 x/N」