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||'')+' |
'+
+ ''+
+ '
'+
+ '表数据行数
'+
+ ''+
+ (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
+