Compare commits

..

1 Commits

Author SHA1 Message Date
Codex eef46f6ec0 fix: verify cloud image flow and clarify save feedback 2026-10-04 22:10:03 +08:00
8 changed files with 72 additions and 7 deletions
+10
View File
@@ -1,5 +1,15 @@
# 赵府智家 · 视觉系统 # 赵府智家 · 视觉系统
## v0.1.32 · 真实网盘验收与保存反馈
- 修复家庭档案已保存、后续列表刷新失败时提示不可见的问题;立即提示保存结果,刷新失败单独提示,无需重复提交。
- 网盘设置中展示封面、平台外观、个人记事、数据库备份四类存储说明;修正“全部本地存储”的旧文案。
- 异常国家编码类型统一返回字段校验错误,不再触发服务器异常,失败保留原档案。
- 真实百度网盘验收:在应用内“验收资料”目录上传 100867 字节合成图片,清空进程缓存后读回校验一致;重复上传返回相同文件 ID。正式家庭的已上传封面也通过独立回读及浏览器刷新、重新打开验证。验收不修改正式家庭档案。
- 手机宽度检查覆盖已上传封面与编辑对话框。自动文件选择器仍受 Edge 扩展文件 URL 权限限制,不将接口上传或用户自行上传等同为自动选文件验收。
验证:150 项 Python、120 项 JavaScript 回归通过。真实网盘和正式封面回读校验通过;360px 网盘说明、390px 封面及对话框无横向溢出。上线前备份数据库,发布源码后部署。
## v0.1.31 · 地域分级选择与网盘图片 ## v0.1.31 · 地域分级选择与网盘图片
- 家庭/场所、空间和家谱籍贯/出生地共用地域控件:中国省市区联动、世界国家/地区选择、自定义历史地名或其他天体。更换上级清空下级;服务端以编码校验父子关系。旧地名无法匹配时原样保留为自定义,权限仍按家庭/场所划分。 - 家庭/场所、空间和家谱籍贯/出生地共用地域控件:中国省市区联动、世界国家/地区选择、自定义历史地名或其他天体。更换上级清空下级;服务端以编码校验父子关系。旧地名无法匹配时原样保留为自定义,权限仍按家庭/场所划分。
+1 -1
View File
@@ -1 +1 @@
0.1.31 0.1.32
+1 -1
View File
@@ -34,7 +34,7 @@ def normalize(value, clean):
children = item.get('children', []) children = item.get('children', [])
elif mode == 'world': elif mode == 'world':
code = value.get('countryCode', '') code = value.get('countryCode', '')
if code not in DATA['countries']: raise ValueError('请选择国家或地区') if not isinstance(code, str) or code not in DATA['countries']: raise ValueError('请选择国家或地区')
if code == 'CN': raise ValueError('中国地区请使用中国分级选择') if code == 'CN': raise ValueError('中国地区请使用中国分级选择')
result.update(countryCode=code, country=DATA['countries'][code]) result.update(countryCode=code, country=DATA['countries'][code])
for field in ('province','city','district'): for field in ('province','city','district'):
+11
View File
@@ -25,6 +25,11 @@ class GeographyUnitTests(unittest.TestCase):
result=self.norm(dict(mode='custom',body='火星',path='基地 / 一区 / 一栋')) result=self.norm(dict(mode='custom',body='火星',path='基地 / 一区 / 一栋'))
self.assertEqual(result['body'],'火星');self.assertEqual(result['countryCode'],'') self.assertEqual(result['body'],'火星');self.assertEqual(result['countryCode'],'')
def test_invalid_country_types_have_a_validation_error(self):
for code in ([], {}, None, 42, True):
with self.subTest(code=code),self.assertRaises(ValueError):
self.norm(dict(mode='world',countryCode=code))
def test_legacy_edits_drop_stale_codes_and_other_updates_preserve(self): def test_legacy_edits_drop_stale_codes_and_other_updates_preserve(self):
old=dict(geography=self.norm(dict(mode='china',provinceCode='37',cityCode='3706')),province='山东省',city='烟台市') old=dict(geography=self.norm(dict(mode='china',provinceCode='37',cityCode='3706')),province='山东省',city='烟台市')
item={};geography.apply(item,{},old,app.clean_text,app.Problem) item={};geography.apply(item,{},old,app.clean_text,app.Problem)
@@ -39,6 +44,12 @@ class GeographyIntegrationTests(unittest.TestCase):
stop_server=appearance_fixture.AppearanceTests.stop_server stop_server=appearance_fixture.AppearanceTests.stop_server
request=appearance_fixture.AppearanceTests.request request=appearance_fixture.AppearanceTests.request
def test_bad_country_input_returns_400_and_preserves_profile(self):
old=app.get_object('households',self.aid)
status,result=self.request('/api/households',self.root,dict(id=self.aid,name='不应写入',geography=dict(mode='world',countryCode=[])))
self.assertEqual(status,400);self.assertIn('国家',result['error'])
self.assertEqual(app.get_object('households',self.aid),old)
def test_household_cover_and_path_roundtrip(self): def test_household_cover_and_path_roundtrip(self):
v=dict(mode='china',provinceCode='37',cityCode='3706',districtCode='370682') v=dict(mode='china',provinceCode='37',cityCode='3706',districtCode='370682')
status,saved=self.request('/api/households',self.root,dict(id=self.aid,name='封面与地域',geography=v,coverData=appearance_fixture.image_data())) status,saved=self.request('/api/households',self.root,dict(id=self.aid,name='封面与地域',geography=v,coverData=appearance_fixture.image_data()))
+43
View File
@@ -30,3 +30,46 @@ test('owner is visibly distinct from family and super administrators',()=>{
assert.equal(c.label({id:'backup',role:'family_admin'}),'家庭管理员'); assert.equal(c.label({id:'backup',role:'family_admin'}),'家庭管理员');
assert.equal(c.label({id:'root',role:'admin'}),'超级管理员'); assert.equal(c.label({id:'root',role:'admin'}),'超级管理员');
}); });
function saveHarness({saveError,refreshError}={}){
const events=[],error={textContent:''},elements=[{disabled:false},{disabled:true}];
elements.id={value:'home'};elements.profileRevision={value:0};
const form={elements,querySelector:()=>error,closest:()=>({close:()=>events.push('closed')})};
let submit;
const state={households:[{id:'home',name:'原档案'}]};
const c=vm.createContext({coverSaving:false,coverDraft:'data:image/png;base64,test',sessionGeneration:1,
$:()=>({addEventListener:(_type,fn)=>{submit=fn;}}),imageJobPending:()=>false,imageJobs:new Map(),
formValues:()=>({id:'home',name:'已保存'}),state,
api:async()=>{if(saveError)throw saveError;return {id:'home',name:'已保存',profileRevision:1,coverRevision:'new',coverRef:{fsId:'1'}};},
renderHouseholds:()=>events.push('rendered'),toast:t=>events.push(t),
refresh:async()=>{events.push('refresh');if(refreshError)throw refreshError;}});
vm.runInContext(part(homes,"$('#household-form').addEventListener('submit'",'document.addEventListener'),c);
return {run:()=>submit({preventDefault(){},currentTarget:form}),events,error,elements,state,c};
}
test('saved cover stays committed and success is visible before a failed refresh',async()=>{
const h=saveHarness({refreshError:Error('network')});await h.run();
assert.equal(h.state.households[0].name,'已保存');assert.equal(h.elements.profileRevision.value,1);
assert.match(h.state.households[0].coverUrl,/v=new/);
assert.ok(h.events.indexOf('封面已存百度网盘,归属档案已保存')<h.events.indexOf('refresh'));
assert.match(h.events.at(-1),/无需重复保存/);assert.ok(h.events.includes('closed'));
assert.deepEqual(h.elements.map(e=>e.disabled),[false,true]);assert.equal(h.c.coverSaving,false);
});
test('failed cover save keeps the draft open and does not claim success',async()=>{
const h=saveHarness({saveError:Error('网盘暂时不可用')});await h.run();
assert.equal(h.state.households[0].name,'原档案');assert.equal(h.error.textContent,'网盘暂时不可用');
assert.deepEqual(h.events,[]);assert.equal(h.c.coverDraft,'data:image/png;base64,test');
});
test('a stale post-save refresh does not show a new-session warning',async()=>{
const h=saveHarness({refreshError:Object.assign(Error('stale'),{stale:true})});await h.run();
assert.equal(h.events.at(-1),'refresh');assert.equal(h.state.households[0].name,'已保存');
});
test('storage guide names cloud categories and avoids old all-local claims',()=>{
const html=fs.readFileSync('web/index.html','utf8');
assert.ok(html.includes('id="baidu-storage-guide"'));
for(const label of ['家庭与场所:','平台外观:','个人记事:','数据库备份:'])assert.ok(html.includes(label));
assert.ok(!html.includes('自有部署 · 本地存储'));assert.ok(!app.includes('数据存于本机'));
});
+1 -1
View File
@@ -94,7 +94,7 @@ function render() {
$('#metric-used').textContent = state.storage.usedGB == null ? '—' : state.storage.usedGB + ' GB'; $('#metric-used').textContent = state.storage.usedGB == null ? '—' : state.storage.usedGB + ' GB';
$('#service-status').textContent = state.mediaOnline ? '视频服务已连接' : '视频服务未连接'; $('#service-status').textContent = state.mediaOnline ? '视频服务已连接' : '视频服务未连接';
$('#service-status').classList.toggle('off', !state.mediaOnline); $('#service-status').classList.toggle('off', !state.mediaOnline);
$('#version').textContent = state.version ? 'v' + state.version + ' / 数据存于本机' : '独立部署 / 数据自有'; $('#version').textContent = state.version ? 'v' + state.version + ' / 独立部署 · 数据自有' : '独立部署 / 数据自有';
const warning = state.storage.paused ? state.storage.reason + ',已暂停录像。' : ''; const warning = state.storage.paused ? state.storage.reason + ',已暂停录像。' : '';
$('#banner').textContent = warning; $('#banner').hidden = !warning; $('#banner').textContent = warning; $('#banner').hidden = !warning;
setOptions($('#site-filter'), '<option value="">全部空间</option>' + state.sites.map(s=>`<option value="${s.id}">${esc(siteName(s.id))}</option>`).join('')); setOptions($('#site-filter'), '<option value="">全部空间</option>' + state.sites.map(s=>`<option value="${s.id}">${esc(siteName(s.id))}</option>`).join(''));
+2 -1
View File
@@ -64,7 +64,8 @@ $('#household-form').addEventListener('submit',async e=>{
const view={...saved,coverUrl:saved.coverData||saved.coverRef?'/api/households/cover?id='+encodeURIComponent(saved.id)+'&v='+saved.coverRevision:''}; const view={...saved,coverUrl:saved.coverData||saved.coverRef?'/api/households/cover?id='+encodeURIComponent(saved.id)+'&v='+saved.coverRevision:''};
delete view.coverData;delete view.coverRef;const previous=(state.households||[]).find(x=>x.id===saved.id); delete view.coverData;delete view.coverRef;const previous=(state.households||[]).find(x=>x.id===saved.id);
if(previous)Object.assign(previous,view);else (state.households||=[]).push(view); if(previous)Object.assign(previous,view);else (state.households||=[]).push(view);
renderHouseholds();f.closest('dialog').close();await refresh();toast(payload.coverData?'封面已存百度网盘,归属档案已保存':payload.coverData===''?'归属档案已保存,已使用默认封面':'归属档案已保存'); renderHouseholds();f.closest('dialog').close();toast(payload.coverData?'封面已存百度网盘,归属档案已保存':payload.coverData===''?'归属档案已保存,已使用默认封面':'归属档案已保存');
try{await refresh();}catch(error){if(!error.stale&&generation===sessionGeneration)toast('档案已保存,列表刷新暂未完成,请稍后刷新页面。无需重复保存。');}
}catch(error){if(!error.stale&&generation===sessionGeneration)f.querySelector('.form-error').textContent=error.message;} }catch(error){if(!error.stale&&generation===sessionGeneration)f.querySelector('.form-error').textContent=error.message;}
finally{coverSaving=false;controls.forEach((x,i)=>x.disabled=disabled[i]);} finally{coverSaving=false;controls.forEach((x,i)=>x.disabled=disabled[i]);}
}); });
+3 -3
View File
@@ -26,7 +26,7 @@
<button data-page="settings">⚙ <span>设置中心</span></button> <button data-page="settings">⚙ <span>设置中心</span></button>
<button data-page="storage">▤ <span>平台存储与审计</span></button> <button data-page="storage">▤ <span>平台存储与审计</span></button>
</nav> </nav>
<div class="side-note"><span class="dot"></span> 自有部署 · 本地存储<p>按空间组织画面<br>让每一段记录有处可寻</p></div> <div class="side-note"><span class="dot"></span> 独立部署 · 数据自有<p>按空间组织画面<br>让每一段记录有处可寻</p></div>
<p id="current-user" class="description"></p><button id="account-password" class="quiet" hidden>修改我的密码</button><button id="logout" class="quiet">退出登录</button> <p id="current-user" class="description"></p><button id="account-password" class="quiet" hidden>修改我的密码</button><button id="logout" class="quiet">退出登录</button>
</aside> </aside>
<main> <main>
@@ -38,14 +38,14 @@
<section id="page-settings" class="page" hidden> <section id="page-settings" class="page" hidden>
<div class="section-head"><h2>把这里布置成你习惯的样子</h2><button id="settings-reload" class="quiet">重新读取设置</button></div><p id="settings-error" role="status"></p><div class="settings-sections" role="group" aria-label="设置分类"><button class="quiet" data-settings-section="preferences" aria-pressed="true">我的偏好</button><button class="quiet" data-settings-section="baidu" data-platform-setting hidden>网盘连接</button><button class="quiet" data-settings-section="brand" data-platform-setting hidden>品牌与外观</button><button class="quiet" data-settings-section="backup" data-platform-setting hidden>数据库备份</button></div> <div class="section-head"><h2>把这里布置成你习惯的样子</h2><button id="settings-reload" class="quiet">重新读取设置</button></div><p id="settings-error" role="status"></p><div class="settings-sections" role="group" aria-label="设置分类"><button class="quiet" data-settings-section="preferences" aria-pressed="true">我的偏好</button><button class="quiet" data-settings-section="baidu" data-platform-setting hidden>网盘连接</button><button class="quiet" data-settings-section="brand" data-platform-setting hidden>品牌与外观</button><button class="quiet" data-settings-section="backup" data-platform-setting hidden>数据库备份</button></div>
<form id="preferences-form" class="panel"><h3>我的偏好</h3><p class="description">只影响当前账号,保存后换电脑或手机登录也会沿用。切换选项可以预览,离开未保存的预览会撤销。</p><div id="personal-choices" class="form-grid"></div><p id="preferences-status" role="status"></p><div class="device-actions"><button type="submit">保存我的偏好</button><button type="button" id="personal-follow" class="quiet">全部跟随平台</button><button type="button" id="personal-cancel" class="quiet">撤销预览</button></div></form> <form id="preferences-form" class="panel"><h3>我的偏好</h3><p class="description">只影响当前账号,保存后换电脑或手机登录也会沿用。切换选项可以预览,离开未保存的预览会撤销。</p><div id="personal-choices" class="form-grid"></div><p id="preferences-status" role="status"></p><div class="device-actions"><button type="submit">保存我的偏好</button><button type="button" id="personal-follow" class="quiet">全部跟随平台</button><button type="button" id="personal-cancel" class="quiet">撤销预览</button></div></form>
<section id="baidu-admin" class="panel" hidden><div class="section-head"><h3>百度网盘 · 统一存储</h3><span class="badge">仅超级管理员配置</span></div><p class="description">连接一个网盘账号,保存各用户上传的照片、视频和语音。系统按用户、家庭和记事可见范围授权;网盘账号持有人本身可以管理网盘中的文件。服务器仅转发分片,不保存媒体原文件。</p><form id="baidu-config"><div class="form-grid"><label>AppKey<input name="appKey" autocomplete="off" maxlength="150" required></label><label>SecretKey<input name="secretKey" type="password" autocomplete="new-password" maxlength="150"></label><label>百度后台应用名称<input name="appFolder" value="智家" required maxlength="40"></label><label>OAuth 回调地址<input name="redirectUri" type="url" placeholder="https://你的域名/api/baidu/callback" required maxlength="400"></label></div><p class="description">回调地址须与百度后台完全一致。SecretKey 留空保持已保存值,SignKey 不需要填写。当前开发者应用仅限个人场景,适用范围以百度后台为准。</p><p id="baidu-config-status" role="status"></p><button type="submit">保存网盘应用配置</button></form><hr><p id="baidu-status" role="status"></p><div class="device-actions"><button id="baidu-connect">连接/重新授权百度网盘</button><button id="baidu-disconnect" class="quiet">解除本机连接</button></div><p class="description">解除连接不会删除云端文件。临时媒体地址由后台按需获取,令牌不发送给浏览器。网盘授权凭据单独保存在服务器受限目录,不包含在数据库备份或公开源码中。</p></section> <section id="baidu-admin" class="panel" hidden><div class="section-head"><h3>百度网盘 · 统一存储</h3><span class="badge">仅超级管理员配置</span></div><p class="description">连接一个网盘账号,统一保存封面、Logo、图标及各用户上传的照片、视频和语音。系统按用户、家庭和记事可见范围授权;网盘账号持有人本身可以管理网盘中的文件。服务器仅转发分片,不保存媒体原文件。</p><details id="baidu-storage-guide"><summary>文件存在哪里?</summary><ul><li>家庭与场所:按家庭或场所保存封面。</li><li>平台外观:保存 Logo 和图标。</li><li>个人记事:按家庭、用户分组,再分照片、视频、语音。</li><li>数据库备份:单独存放;新备份上传并校验成功后,清理超过一个自然月的旧云备份。</li></ul><p class="description">文字、权限和文件引用保存在本机数据库;个人媒体原文件保存在百度网盘。封面和品牌图片仅做有限的内存缓存。更换封面、移除附件或删除记事不会删除网盘原文件。</p></details><form id="baidu-config"><div class="form-grid"><label>AppKey<input name="appKey" autocomplete="off" maxlength="150" required></label><label>SecretKey<input name="secretKey" type="password" autocomplete="new-password" maxlength="150"></label><label>百度后台应用名称<input name="appFolder" value="智家" required maxlength="40"></label><label>OAuth 回调地址<input name="redirectUri" type="url" placeholder="https://你的域名/api/baidu/callback" required maxlength="400"></label></div><p class="description">回调地址须与百度后台完全一致。SecretKey 留空保持已保存值,SignKey 不需要填写。当前开发者应用仅限个人场景,适用范围以百度后台为准。</p><p id="baidu-config-status" role="status"></p><button type="submit">保存网盘应用配置</button></form><hr><p id="baidu-status" role="status"></p><div class="device-actions"><button id="baidu-connect">连接/重新授权百度网盘</button><button id="baidu-disconnect" class="quiet">解除本机连接</button></div><p class="description">解除连接不会删除云端文件。临时媒体地址由后台按需获取,令牌不发送给浏览器。网盘授权凭据单独保存在服务器受限目录,不包含在数据库备份或公开源码中。</p></section>
<div id="settings-private" hidden> <div id="settings-private" hidden>
<form id="brand-form" class="panel"><h3>发布与品牌配置</h3><p class="description">仅超级管理员可修改。名称、Logo、浏览器图标及登录文案会公开显示;适用于整套系统。已有个人偏好优先于平台默认。</p> <form id="brand-form" class="panel"><h3>发布与品牌配置</h3><p class="description">仅超级管理员可修改。名称、Logo、浏览器图标及登录文案会公开显示;适用于整套系统。已有个人偏好优先于平台默认。</p>
<div class="form-grid"><label>系统名称<input name="name" required maxlength="40"></label><label>系统副标题<input name="subtitle" required maxlength="60"></label><label class="full">页首题词<input name="motto" maxlength="100"></label><label>登录标题<input name="loginTitle" required maxlength="40"></label><label>登录说明<input name="loginNote" maxlength="160"></label><label class="full">页脚说明<input name="footer" maxlength="120"></label></div> <div class="form-grid"><label>系统名称<input name="name" required maxlength="40"></label><label>系统副标题<input name="subtitle" required maxlength="60"></label><label class="full">页首题词<input name="motto" maxlength="100"></label><label>登录标题<input name="loginTitle" required maxlength="40"></label><label>登录说明<input name="loginNote" maxlength="160"></label><label class="full">页脚说明<input name="footer" maxlength="120"></label></div>
<div class="brand-image-grid"><div><img id="brand-logo-preview" alt="Logo 预览" src="/zhao-icon.png"><label>更换系统 Logo<input id="brand-logo-file" type="file" accept="image/png,image/jpeg,image/webp"></label><button type="button" id="brand-logo-reset" class="quiet">使用默认 Logo</button></div><div><img id="brand-icon-preview" alt="浏览器图标预览" src="/zhao-icon.png"><label>更换浏览器图标<input id="brand-icon-file" type="file" accept="image/png,image/jpeg,image/webp"></label><button type="button" id="brand-icon-reset" class="quiet">使用默认图标</button></div></div><p class="description">支持 PNG、JPEG、WebP,原图最多 5 MB;上传时自动缩小,图片随系统数据库保存。</p> <div class="brand-image-grid"><div><img id="brand-logo-preview" alt="Logo 预览" src="/zhao-icon.png"><label>更换系统 Logo<input id="brand-logo-file" type="file" accept="image/png,image/jpeg,image/webp"></label><button type="button" id="brand-logo-reset" class="quiet">使用默认 Logo</button></div><div><img id="brand-icon-preview" alt="浏览器图标预览" src="/zhao-icon.png"><label>更换浏览器图标<input id="brand-icon-file" type="file" accept="image/png,image/jpeg,image/webp"></label><button type="button" id="brand-icon-reset" class="quiet">使用默认图标</button></div></div><p class="description">支持 PNG、JPEG、WebP,原图最多 5 MB;上传时自动缩小,图片随系统数据库保存。</p>
<h3>新账号与跟随平台账号的默认外观</h3><div id="brand-defaults" class="form-grid"></div><p id="brand-status" role="status"></p><div class="device-actions"><button type="submit">保存发布配置</button><button type="button" id="brand-export" class="quiet">导出发布配置</button><label class="config-import">导入配置到表单<input id="brand-import" type="file" accept="application/json,.json"></label></div><p class="description">导入后先核对再保存。导出的配置只含品牌与默认外观,不含账号、设备凭据、家庭资料和网络备份目录。</p> <h3>新账号与跟随平台账号的默认外观</h3><div id="brand-defaults" class="form-grid"></div><p id="brand-status" role="status"></p><div class="device-actions"><button type="submit">保存发布配置</button><button type="button" id="brand-export" class="quiet">导出发布配置</button><label class="config-import">导入配置到表单<input id="brand-import" type="file" accept="application/json,.json"></label></div><p class="description">导入后先核对再保存。导出的配置只含品牌与默认外观,不含账号、设备凭据、家庭资料和网络备份目录。</p>
</form> </form>
<section id="backup-panel" class="panel"><h3>数据库备份</h3><p class="description">整台服务的数据库包含所有家庭、账号、设备连接凭据和图片,因此仅超级管理员可操作。录像文件不包含在数据库副本中;恢复后所有账号需要重新登录。</p><form id="backup-form"><div class="form-grid"><label class="check full"><input name="enabled" type="checkbox">启用每日定时备份</label><label class="check full"><input name="baidu" type="checkbox">同时上传到已连接的百度网盘</label><label>每日时间(北京时间)<input name="time" type="time" required value="03:00"></label><label>本机/挂载目录保留份数<input name="keep" type="number" min="1" max="90" required value="14"></label><label class="full">网络备份目录(可留空)<input name="networkPath" maxlength="500" placeholder="/var/lib/zhaovision/network-backup"></label></div><p class="description">填写本服务器已挂载的网络盘目录。使用百度接口备份只需勾选上方选项;此路径用于其他已挂载的网络存储,不能填网页地址或分享链接。写入网络目录成功不等于已确认百度云端同步完成。</p><p class="description">默认部署允许写入 /var/lib/zhaovision 下的目录。挂载和目录权限须由主机管理员配置;本页不会保存网盘登录密码。保留份数只清理本机和挂载目录中本系统生成的旧备份,百度网盘保留最近一个自然月;新备份上传并校验成功后才清理本应用的过期副本。</p><p id="backup-local-path" class="description"></p><p id="backup-save-status" role="status"></p><button type="submit">保存备份设置</button></form><div class="section-head"><h3>备份记录</h3><button id="backup-run">立即备份数据库</button></div><p id="backup-result" role="status"></p><div id="backup-history"></div></section> <section id="backup-panel" class="panel"><h3>数据库备份</h3><p class="description">整台服务的数据库包含所有家庭、账号、设备连接配置和网盘文件引用,因此仅超级管理员可操作。网盘中的照片、视频、语音及录像文件不包含在新数据库副本中;旧版备份可能仍含历史内嵌图片。恢复后所有账号需要重新登录。</p><form id="backup-form"><div class="form-grid"><label class="check full"><input name="enabled" type="checkbox">启用每日定时备份</label><label class="check full"><input name="baidu" type="checkbox">同时上传到已连接的百度网盘</label><label>每日时间(北京时间)<input name="time" type="time" required value="03:00"></label><label>本机/挂载目录保留份数<input name="keep" type="number" min="1" max="90" required value="14"></label><label class="full">网络备份目录(可留空)<input name="networkPath" maxlength="500" placeholder="/var/lib/zhaovision/network-backup"></label></div><p class="description">填写本服务器已挂载的网络盘目录。使用百度接口备份只需勾选上方选项;此路径用于其他已挂载的网络存储,不能填网页地址或分享链接。写入网络目录成功不等于已确认百度云端同步完成。</p><p class="description">默认部署允许写入 /var/lib/zhaovision 下的目录。挂载和目录权限须由主机管理员配置;本页不会保存网盘登录密码。保留份数只清理本机和挂载目录中本系统生成的旧备份,百度网盘保留最近一个自然月;新备份上传并校验成功后才清理本应用的过期副本。</p><p id="backup-local-path" class="description"></p><p id="backup-save-status" role="status"></p><button type="submit">保存备份设置</button></form><div class="section-head"><h3>备份记录</h3><button id="backup-run">立即备份数据库</button></div><p id="backup-result" role="status"></p><div id="backup-history"></div></section>
</div> </div>
</section> </section>
<section id="page-journal" class="page" hidden> <section id="page-journal" class="page" hidden>