diff --git a/static/admin/base.js b/static/admin/base.js index 88ac53b..2c23911 100644 --- a/static/admin/base.js +++ b/static/admin/base.js @@ -102,11 +102,12 @@ function showTab(name){ if(name==='logs'){loadLogs();if(document.getElementById('log-auto').checked)_logTimer=setInterval(loadLogs,3000);} if(name==='agent' && typeof initAgentChat==='function') initAgentChat(); if(name==='users')loadUsers(); + if(name==='system'){showSubTab('system',_activeSubs.system||'backup');} } // ================== 页内子分栏(任务/工具 通用) ================== // 每个带子分栏的 Tab 记住上次选中的子分栏,切走再切回来保持原位 -let _activeSubs = {tasks: 'plan', tools: 'clipboard'}; +let _activeSubs = {tasks: 'plan', tools: 'clipboard', system: 'backup'}; let _discoveryTimer = null; // 设备自动发现 10s 轮询(仅 devpool 子分栏激活时) function showSubTab(tabId, name){ diff --git a/static/admin/system.js b/static/admin/system.js new file mode 100644 index 0000000..c2005ac --- /dev/null +++ b/static/admin/system.js @@ -0,0 +1,125 @@ +// 「系统」Tab:数据备份导出/导入(依赖 base.js:esc/_csrfHeaders/apiPost/showToast/handleResult) +let _restoreToken = ''; // 预览通过后的暂存标识(用于 apply) + +// ================== 导出 ================== +async function doExportBackup(){ + const st=document.getElementById('backup-status'); + const btn=event&&event.target||null; + if(btn)btn.disabled=true; + const includeApk=document.getElementById('backup-include-apk').checked; + if(st)st.textContent='正在生成备份并打包…'; + try{ + const r=await fetch('/api/system/backup/export',{ + method:'POST', + headers:{'Content-Type':'application/json',..._csrfHeaders()}, + body:JSON.stringify({include_apk:includeApk}) + }); + if(r.status===401){window.location='/login';return;} + if(!r.ok){ + let msg='导出失败'; + try{const d=await r.json();if(d&&d.error)msg=d.error;}catch(e){} + showToast(msg,'error'); + if(st)st.textContent=''; + return; + } + const blob=await r.blob(); + const cd=r.headers.get('Content-Disposition')||''; + const m=/filename="?([^";]+)"?/i.exec(cd); + const fname=m?m[1]:('auto_control_backup_'+Date.now()+'.zip'); + const url=URL.createObjectURL(blob); + const a=document.createElement('a'); + a.href=url;a.download=fname;document.body.appendChild(a); + a.click();a.remove(); + setTimeout(()=>URL.revokeObjectURL(url),3000); + if(st)st.textContent='已生成 '+fname; + showToast('导出成功','success'); + }catch(e){ + showToast('导出失败: '+e,'error'); + if(st)st.textContent=''; + }finally{ + if(btn)btn.disabled=false; + } +} + +// ================== 导入:上传预览 ================== +async function previewRestore(){ + const fileEl=document.getElementById('restore-file'); + const f=fileEl&&fileEl.files&&fileEl.files[0]; + const box=document.getElementById('restore-preview'); + const st=document.getElementById('restore-status'); + const applyBtn=document.getElementById('btn-apply-restore'); + if(!f){showToast('请先选择备份文件(.zip 或 .db)','error');return;} + _restoreToken=''; + if(applyBtn)applyBtn.style.display='none'; + if(st)st.textContent=''; + box.innerHTML='解析校验中…'; + const fd=new FormData(); + fd.append('file',f); + try{ + const r=await fetch('/api/system/backup/preview',{ + method:'POST', + headers:_csrfHeaders(), // FormData 不能手动设 Content-Type + body:fd + }); + if(r.status===401){window.location='/login';return;} + const d=await r.json(); + box.innerHTML=''; + if(!d.ok){showToast(d.error||'解析失败','error');return;} + _restoreToken=d.token; + renderRestorePreview(d.preview); + if(applyBtn)applyBtn.style.display=''; + }catch(e){ + box.innerHTML=''; + showToast('上传失败: '+e,'error'); + } +} + +function renderRestorePreview(p){ + const box=document.getElementById('restore-preview'); + const rows=(p.tables||[]).map(t=> + ''+esc(t.label||t.table)+''+esc(t.table)+''+esc(t.rows)+'').join(''); + const warns=(p.warnings||[]).map(w=>'
  • '+esc(w)+'
  • ').join(''); + box.innerHTML= + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + '
    文件'+esc(p.file_name||'')+'('+(p.file_size!=null?fmtSize(p.file_size):'')+')
    库结构版本备份 v'+esc(p.schema_version)+' / 当前 v'+esc(p.current_schema_version)+'
    完整性'+esc(p.integrity||'')+'
    '+ + '
    表数据行数
    '+ + ''+(rows||'')+'
    业务表行数
    —
    '+ + (warns?'
    ':''); +} + +function fmtSize(n){ + if(n==null)return ''; + if(n<1024)return n+' B'; + if(n<1024*1024)return (n/1024).toFixed(1)+' KB'; + return (n/1024/1024).toFixed(1)+' MB'; +} + +// ================== 导入:确认应用 ================== +async function applyRestore(){ + const applyBtn=document.getElementById('btn-apply-restore'); + const st=document.getElementById('restore-status'); + if(!_restoreToken)return; + if(!confirm('确认应用导入?\n\n系统会先自动备份当前库(data/backups/pre_restore_*.db),再把待导入数据覆盖现有全部数据。\n导入将在重启 web_server 后生效。\n\n确定继续?'))return; + if(applyBtn)applyBtn.disabled=true; + if(st)st.textContent='正在生成恢复任务…'; + const r=await apiPost('/api/system/backup/apply',{token:_restoreToken}); + if(!r)return; + if(r.ok){ + const box=document.getElementById('restore-preview'); + box.innerHTML='
    '+ + '✅ 恢复任务已生成。
    当前库已自动备份到 '+esc(r.backup_name||'')+'。
    '+ + '请重启 web_server,启动时将自动应用导入数据(恢复完成前不要再次导入)。
    '; + if(st)st.textContent=''; + if(applyBtn)applyBtn.style.display='none'; + document.getElementById('restore-file').value=''; + _restoreToken=''; + }else{ + showToast(r.error||'应用导入失败','error'); + if(applyBtn)applyBtn.disabled=false; + } +} diff --git a/templates/admin/monitor.html b/templates/admin/monitor.html index 7350eb2..84a434c 100644 --- a/templates/admin/monitor.html +++ b/templates/admin/monitor.html @@ -365,6 +365,7 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14 +
    ... @@ -850,9 +851,51 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
    管理设备分组,用于任务按分组执行。分组内的设备需先在设备池管理中确认(不在池内的设备不参与调度)。
    + + +
    +
    系统
    +
    系统数据备份导出/导入(仅管理员)
    +
    + + +
    + +
    +
    + 导出当前系统数据为 zip:含 users.db(用户 / 任务计划 / 设备分组 / 自定义动作 / + 设备池 / AI 会话 / 经验库等全部表,SQLite 在线一致快照)+ 可选的 apks/ 应用文件。 + 恢复用:换机 / 整库迁移 / 出问题前留底。 +
    +
    + + + +
    +
    + ⚠️ 备份为全量快照:含用户口令哈希、AI 配置里的 API Key 等敏感信息,请妥善保管,勿外传、勿提交到 git。 +
    +
    + + +
    +
    + 导入恢复:上传平台导出的 .zip 或 .db → 先校验预览 → + 确认后系统会自动备份当前库并把恢复任务挂起,重启 web_server 后生效(导入将覆盖现有全部数据)。 +
    +
    + + + + +
    +
    +
    @@ -918,5 +961,6 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14 +