prts-moonlit 1.2.1 → 1.2.3

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.
Files changed (4) hide show
  1. package/gacha.html +149 -9
  2. package/index.html +175 -18
  3. package/ledger.html +179 -10
  4. package/package.json +1 -1
package/gacha.html CHANGED
@@ -1049,7 +1049,7 @@ document.addEventListener('pointerdown',function(){ac()},{once:true});
1049
1049
  <script>
1050
1050
  /* ===== SYSTEM LOGIN · 云存档同步引擎 v1 ===== */
1051
1051
  (function(){
1052
- var TK=['whale.tasks.v1','minis_ledger_v1','minis_ledger_trash_v1','whale.bgm','whale.taro.v1','whale.ai.cfg.v1','whale.ai.psn.v1','whale.ai.chat.v1','whale.ai.memo.v1'];
1052
+ var TK=['whale.tasks.v1','minis_ledger_v1','minis_ledger_trash_v1','whale.bgm','whale.taro.v1','whale.ai.cfg.v1','whale.ai.psn.v1','whale.ai.chat.v1','whale.ai.memo.v1','whale.bal.v1'];
1053
1053
  var BASE='https://textdb.dev/api/data/prts-acct-';var ADMIN_K='e74aeb1a34c44f2fcefd1f19bb34c3891a4963f36242e5a5fd3869085892a711';
1054
1054
  var LS='whale.acct',LT='whale.sync.t';
1055
1055
  var acct=null;try{acct=JSON.parse(localStorage.getItem(LS)||'null')}catch(e){}
@@ -1083,6 +1083,33 @@ document.addEventListener('pointerdown',function(){ac()},{once:true});
1083
1083
  }
1084
1084
  function snap(){var d={};for(var i=0;i<TK.length;i++){var v=localStorage.getItem(TK[i]);if(v!==null)d[TK[i]]=v;}return d;}
1085
1085
  function url(){return BASE+acct.k;}
1086
+ function mbFetch(cb){
1087
+ try{
1088
+ var x=new XMLHttpRequest();x.open('GET','https://textdb.dev/api/data/prts-myb-mailbox?ts='+Date.now(),true);x.timeout=20000;
1089
+ x.onload=function(){try{var cur=JSON.parse((''+x.responseText).replace(/=$/,''))||[];if(!(cur instanceof Array))cur=[];cb(cur)}catch(e){cb(null)}};
1090
+ x.onerror=function(){cb(null)};x.send();
1091
+ }catch(e){cb(null)}
1092
+ }
1093
+ function mbEsc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
1094
+ function mbRender(){
1095
+ var In=document.getElementById('mbIn');if(!In)return;
1096
+ if(!(window.__acct&&window.__acct.admin&&window.__acct.admin())){
1097
+ In.innerHTML='<div style="font-size:10px;opacity:.65">📬 收件箱 · 仅主人可见(先用你的账号登录再来看)</div>';return;
1098
+ }
1099
+ In.innerHTML='<div style="font-size:10px;opacity:.7">⏳ 收信中…</div>';
1100
+ mbFetch(function(list){
1101
+ var In2=document.getElementById('mbIn');if(!In2)return;
1102
+ if(list===null){In2.innerHTML='<div style="font-size:10px;opacity:.7">⚠ 收信失败 <button id="mbRe" style="font-size:10px">重试</button></div>';bindRe();return;}
1103
+ var h='<div style="font-size:11px;opacity:.85;margin-bottom:6px">📬 收件箱 <span style="opacity:.55;font-size:10px">共 '+list.length+' 封</span><button id="mbRe" style="float:right;font-size:10px">↻ 刷新</button></div>';
1104
+ for(var i=0;i<list.length;i++){var m=list[i]||{};var d=new Date(m.ts||0);
1105
+ var ts=('0'+(d.getMonth()+1)).slice(-2)+'-'+('0'+d.getDate()).slice(-2)+' '+('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2);
1106
+ h+='<div style="border:1px solid #1d3550;border-radius:3px;padding:6px 8px;margin-bottom:5px;background:#0a1220"><div style="font-size:10px;opacity:.75"><b style="color:'+(m.k==='战绩'?'#ffd66b':'#8ff0d0')+'">'+(m.k==='战绩'?'⚔ 战绩':'📮 意见')+'</b> · '+ts+'</div><div style="font-size:11px;color:#dff1ff;margin-top:3px;white-space:pre-wrap;word-break:break-word">'+mbEsc(m.t||'')+'</div></div>';
1107
+ }
1108
+ if(!list.length)h+='<div style="font-size:10px;opacity:.6">还没有来信~</div>';
1109
+ In2.innerHTML=h;bindRe();
1110
+ function bindRe(){var r3=document.getElementById('mbRe');if(r3)r3.onclick=function(){mbRender();};}
1111
+ });
1112
+ }
1086
1113
  function mbPost(kind,txt,cb){
1087
1114
  var u='https://textdb.dev/api/data/prts-myb-mailbox';
1088
1115
  try{
@@ -1121,17 +1148,17 @@ document.addEventListener('pointerdown',function(){ac()},{once:true});
1121
1148
  wxhr(url(),JSON.stringify({v:1,u:acct.u,c:acct.c||0,t:t,data:snap()}),function(){st('✔ 已上传 · 后台校验…');setTimeout(function(){verify(t)},600);});
1122
1149
  }
1123
1150
  function verify(t){
1124
- fetch(url()+'?ts='+Date.now(),{cache:'no-store'}).then(function(r){return r.text()}).then(function(x){
1151
+ var x2=new XMLHttpRequest();x2.open('GET',url()+'?ts='+Date.now(),true);x2.timeout=20000;x2.onload=function(){try{var x=x2.responseText;
1125
1152
  x=(x||'').replace(/=$/,'');
1126
1153
  try{var rb=JSON.parse(x);if(rb.t===t){st('✔ 已同步 '+clock());return;}}catch(e){}
1127
1154
  if(!window.__pv){window.__pv=1;setTimeout(function(){push(1);},800);}
1128
1155
  else{window.__pv=0;st('⚠ 上传好像没送达(此浏览器限制跨域)——数据还在本机,可换 Safari 或用存档码');}
1129
- }).catch(function(){st('⚠ 校验失败(可能网络受限)');});
1156
+ }catch(e){st('⚠ 校验失败(可能网络受限)');}};x2.onerror=x2.ontimeout=function(){st('⚠ 校验失败(可能网络受限)');};x2.send();
1130
1157
  }
1131
1158
  function pull(manual){
1132
1159
  if(!acct||!acct.k)return;
1133
1160
  if(manual)st('↻ 同步中…');
1134
- fetch(url()+'?ts='+Date.now(),{cache:'no-store'}).then(function(r){return r.text()}).then(function(x){
1161
+ var x2=new XMLHttpRequest();x2.open('GET',url()+'?ts='+Date.now(),true);x2.timeout=20000;x2.onload=function(){try{var x=x2.responseText;
1135
1162
  x=(x||'').replace(/=$/,'');
1136
1163
  if(!x){if(manual)st('☁ 云端还没有存档——点「云端上传」把本机数据传上去');return;}
1137
1164
  var rb;try{rb=JSON.parse(x)}catch(e){st('⚠ 云端数据格式异常');return;}
@@ -1146,7 +1173,7 @@ document.addEventListener('pointerdown',function(){ac()},{once:true});
1146
1173
  st(same2?'✔ 已是最新 '+clock():'☁ 云端比本机旧——点「云端上传」可覆盖云端');
1147
1174
  }
1148
1175
  else{st('✔ 已是云端最新');}
1149
- }).catch(function(){st('⚠ 云端连接失败');});
1176
+ }catch(e){st('⚠ 云端数据格式异常');}};x2.onerror=x2.ontimeout=function(){st('⚠ 云端连接失败');};x2.send();
1150
1177
  }
1151
1178
  function logout(){localStorage.removeItem(LS);acct=null;}
1152
1179
  var CSS='.acWrap{position:fixed;inset:0;z-index:9998;background:rgba(3,6,14,.66);display:none;align-items:center;justify-content:center}.acWrap.on{display:flex}'+
@@ -1174,11 +1201,15 @@ document.addEventListener('pointerdown',function(){ac()},{once:true});
1174
1201
  }
1175
1202
  h+='<button id="acExp">💾 本地存档码</button><button id="acImp">📥 存档码导入</button><button id="acWipe" style="border-color:#ff5f57;color:#ff8a84">🧹 清空本机存档</button>'+
1176
1203
  '<button id="acFb" style="border-color:#ffd23f;color:#ffd66b">📮 意见箱</button>'+
1204
+ '<button id="vbBtn" style="border-color:#7fe0c0;color:#8ff0d0">🔐 本地密码本</button>'+
1205
+ (window.__acct&&window.__acct.admin&&window.__acct.admin()?'<button id="mbBtn" style="border-color:#9fd0ff;color:#c8e4ff">📬 收件箱</button>':'')+
1177
1206
  '<div class="st" id="acSt"></div><textarea id="acTa" spellcheck="false"></textarea>'+
1178
1207
  '<div id="fbBox" style="display:none;margin-top:10px;border-top:1px dashed #2b4a66;padding-top:9px">'+
1179
1208
  '<div style="font-size:11px;opacity:.78;margin-bottom:5px">📮 意见箱 · 内容直达云端信箱(可匿名),鲸鲸会定期收信</div>'+
1180
1209
  '<textarea id="fbTxt" spellcheck="false" style="display:block;height:64px" placeholder="想说的话、想要的更新、发现的 bug……"></textarea>'+
1181
1210
  '<div style="margin-top:6px"><button id="fbSend" style="border-color:#ffd23f;color:#ffd66b">寄到云端</button><span id="fbSt" style="font-size:10px;opacity:.75;margin-left:8px"></span></div></div>'+
1211
+ '<div id="vbBox" style="display:none;margin-top:10px;border-top:1px dashed #2b4a66;padding-top:9px"><div id="vbIn"></div></div>'+
1212
+ '<div id="mbBox" style="display:none;margin-top:10px;border-top:1px dashed #2b4a66;padding-top:9px"><div id="mbIn"></div></div>'+
1182
1213
  '<div style="font-size:10px;opacity:.5;margin-top:9px;line-height:1.5">密钥由「用户名+密码」派生;同一账号在任意设备登录即接回数据。忘记密码=新仓库(旧数据找不回)</div>'+
1183
1214
  '<button id="acX" style="border-color:#2b4a66;color:#8ea6bf;margin-top:10px">关闭 / CLOSE</button></div>';
1184
1215
  wrap.innerHTML=h;
@@ -1197,7 +1228,7 @@ document.addEventListener('pointerdown',function(){ac()},{once:true});
1197
1228
  st('✦ 仓库已创建,正在上传本机数据…');render();push();return;
1198
1229
  }
1199
1230
  st('↻ 查找云端仓库…');
1200
- fetch(BASE+k+'?ts='+Date.now(),{cache:'no-store'}).then(function(r){return r.text()}).then(function(x){
1231
+ var x3=new XMLHttpRequest();x3.open('GET',BASE+k+'?ts='+Date.now(),true);x3.timeout=20000;x3.onload=function(){try{var x=x3.responseText;
1201
1232
  x=(x||'').replace(/=$/,'');
1202
1233
  if(x&&x.length>2){
1203
1234
  var rb=null;try{rb=JSON.parse(x)}catch(e){}if(!rb){acct=null;localStorage.removeItem(LS);st('⚠ 云端数据异常——没敢覆盖');return;}
@@ -1208,7 +1239,7 @@ document.addEventListener('pointerdown',function(){ac()},{once:true});
1208
1239
  b.setAttribute('data-mode','create');b.textContent='✦ 没找到仓库——点此以本机数据创建';
1209
1240
  st('(如果是新账号,创建后本机数据会上传成初始存档)');
1210
1241
  }
1211
- }).catch(function(){st('⚠ 云端连接失败,稍后再试');});
1242
+ }catch(e){st('⚠ 云端连接失败,稍后再试');}};x3.onerror=x3.ontimeout=function(){st('⚠ 云端连接失败,稍后再试');};x3.send();
1212
1243
  };
1213
1244
  var pl=g('#acPull');if(pl)pl.onclick=function(){pull(1)};
1214
1245
  var pu=g('#acPush');if(pu)pu.onclick=function(){if(acct&&acct.k)push();};
@@ -1233,7 +1264,7 @@ document.addEventListener('pointerdown',function(){ac()},{once:true});
1233
1264
  var wp=g('#acWipe');
1234
1265
  if(wp)wp.onclick=function(){
1235
1266
  if(wp.getAttribute('data-c')){
1236
- try{var ks=[],kk;for(var i=0;i<localStorage.length;i++){kk=localStorage.key(i);if(/^(whale\.|minis_ledger)/.test(kk))ks.push(kk);}
1267
+ try{var ks=[],kk;for(var i=0;i<localStorage.length;i++){kk=localStorage.key(i);if(/^(whale\.|minis_ledger)/.test(kk)&&kk.indexOf('whale.vault')!==0)ks.push(kk);}
1237
1268
  for(var j=0;j<ks.length;j++)localStorage.removeItem(ks[j]);}catch(e){}
1238
1269
  location.reload();return;}
1239
1270
  wp.setAttribute('data-c','1');wp.textContent='⚠ 再点一次=清空并刷新(云端不受影响)';};
@@ -1245,6 +1276,8 @@ document.addEventListener('pointerdown',function(){ac()},{once:true});
1245
1276
  if(st2)st2.textContent='寄送中…';
1246
1277
  mbPost('意见',t2,function(ok){if(st2)st2.textContent=ok?'✅ 已寄出,谢谢!鲸鲸会看到的':'⚠ 寄送失败,稍后再试';if(ok&&t)t.value='';});
1247
1278
  };
1279
+ var mbb=g('#mbBtn');if(mbb)mbb.onclick=function(){var b=g('#mbBox');if(!b)return;var op=(!b.style.display||b.style.display==='none');b.style.display=op?'block':'none';if(op)mbRender();};
1280
+ var vbb=g('#vbBtn');if(vbb)vbb.onclick=function(){var b=g('#vbBox');if(!b)return;var op=(!b.style.display||b.style.display==='none');b.style.display=op?'block':'none';if(op){vbLoad();vbRender();}};
1248
1281
  var x=g('#acX');if(x)x.onclick=function(){wrap.classList.remove('on')};
1249
1282
  wrap.onclick=function(e){if(e.target===wrap)wrap.classList.remove('on')};
1250
1283
  }
@@ -1267,7 +1300,7 @@ document.addEventListener('pointerdown',function(){ac()},{once:true});
1267
1300
  btn.addEventListener('click',open);
1268
1301
  setInterval(function(){if(acct&&acct.k){var c=canon();if(c!==lastSnap){lastSnap=c;clearTimeout(pq);pq=setTimeout(push,900);}}},2000);
1269
1302
  if(acct&&acct.k){setTimeout(function(){pull(false);},250);}
1270
- var APP_VER='1.2.1';
1303
+ var APP_VER='1.2.3';
1271
1304
  function _verCmp(a,b){var x=String(a).split('.'),y=String(b).split('.');for(var i=0;i<3;i++){var p=parseInt(x[i]||'0',10),q=parseInt(y[i]||'0',10);if(p!==q)return p-q}return 0}
1272
1305
  function chkVer(){
1273
1306
  try{
@@ -1289,6 +1322,113 @@ function chkVer(){
1289
1322
  }
1290
1323
  setTimeout(chkVer,2600);
1291
1324
  setInterval(chkVer,600000);
1325
+ /* ===== 🔐 本地密码本(whale.vault.v1 · 只存本机 · 不入同步/存档码/清空存档)===== */
1326
+ var VBK='whale.vault.v1',vbMem=null,vbEdit=-1;
1327
+ function vbRead(){try{return JSON.parse(localStorage.getItem(VBK)||'null')}catch(e){return null}}
1328
+ function vbSave(o){try{localStorage.setItem(VBK,JSON.stringify(o));return 1}catch(e){return 0}}
1329
+ function vbCache(s){try{if(s===null)sessionStorage.removeItem('whale.vault.un');else sessionStorage.setItem('whale.vault.un',s)}catch(e){}}
1330
+ function vbCached(){try{return sessionStorage.getItem('whale.vault.un')||''}catch(e){return ''}}
1331
+ function vbSalt(){var s='',h='0123456789abcdef';for(var i=0;i<16;i++)s+=h.charAt(Math.floor(Math.random()*16));return s;}
1332
+ function vbKey(pass,salt,iter){var x=salt+'|'+pass;for(var i=0;i<iter;i++)x=sha(x);return x;}
1333
+ function vbXor(s,key){var o='';for(var b=0;b<s.length;b+=32){var blk=sha(key+'|'+(b/32|0));
1334
+ for(var j=0;j<32&&b+j<s.length;j++)o+=String.fromCharCode(s.charCodeAt(b+j)^parseInt(blk.substr(j*2,2),16));}return o;}
1335
+ function vbEnc(t,key){return btoa(vbXor(unescape(encodeURIComponent(t)),key));}
1336
+ function vbDec(c,key){return decodeURIComponent(escape(vbXor(atob(c),key)));}
1337
+ function vbEsc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
1338
+ function vbMsg(t){var m=document.getElementById('vbMsg');if(m)m.textContent=t;}
1339
+ function vbCopy(t){var ok=0;try{navigator.clipboard.writeText(t);ok=1}catch(e){}
1340
+ if(!ok){try{var ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();ok=document.execCommand('copy');document.body.removeChild(ta)}catch(e2){}}
1341
+ return ok;}
1342
+ function vbLoad(){
1343
+ var v=vbRead();if(!v){return}
1344
+ if(!v.lock){vbMem={list:(v.list||[]),key:''};return}
1345
+ var ck=vbCached();if(!ck){vbMem=null;return}
1346
+ try{vbMem={list:JSON.parse(vbDec(v.blob,ck)),key:ck}}catch(e){vbCache(null);vbMem=null}
1347
+ }
1348
+ function vbPersist(){
1349
+ var v=vbRead();if(!v||!vbMem)return;
1350
+ if(v.lock){v.blob=vbEnc(JSON.stringify(vbMem.list),vbMem.key)}
1351
+ else{v.list=vbMem.list}
1352
+ vbSave(v);
1353
+ }
1354
+ function vbRender(){
1355
+ var In=document.getElementById('vbIn');if(!In)return;
1356
+ var v=vbRead(),h='';
1357
+ if(!v){
1358
+ h='<div style="font-size:11px;opacity:.85;margin-bottom:5px">🔐 本地密码本 · <span style="opacity:.55;font-size:10px">只存本机 · 永不云同步 · 不随存档导出</span></div>'
1359
+ +'<div style="font-size:10px;opacity:.6;margin-bottom:6px">第一次使用:设一个主口令(留空 = 不加锁)</div>'
1360
+ +'<input id="vbPass" type="password" placeholder="主口令(可留空)" style="width:100%;box-sizing:border-box;background:#0b1424;border:1px solid #2b4a66;color:#dff1ff;border-radius:3px;padding:7px 9px;font:12px ui-monospace,Menlo,monospace">'
1361
+ +'<div style="margin-top:6px"><button id="vbGo" style="border-color:#7fe0c0;color:#8ff0d0">创建密码本</button><span id="vbMsg" style="font-size:10px;opacity:.75;margin-left:8px"></span></div>';
1362
+ In.innerHTML=h;
1363
+ document.getElementById('vbGo').onclick=function(){
1364
+ var p=(document.getElementById('vbPass')||{}).value||'';
1365
+ if(!p){vbSave({v:1,lock:0,list:[]});vbMem={list:[],key:''};vbRender();return}
1366
+ var salt=vbSalt(),iter=3000,key=vbKey(p,salt,iter);
1367
+ vbSave({v:1,lock:1,salt:salt,iter:iter,blob:vbEnc('[]',key)});
1368
+ vbCache(key);vbMem={list:[],key:key};vbRender();
1369
+ };
1370
+ return;
1371
+ }
1372
+ if(v.lock&&!vbMem){
1373
+ h='<div style="font-size:11px;opacity:.85;margin-bottom:5px">🔐 本地密码本 · <b style="color:#ffd23f">已锁定</b></div>'
1374
+ +'<input id="vbPass" type="password" placeholder="主口令" style="width:100%;box-sizing:border-box;background:#0b1424;border:1px solid #2b4a66;color:#dff1ff;border-radius:3px;padding:7px 9px;font:12px ui-monospace,Menlo,monospace">'
1375
+ +'<div style="margin-top:6px"><button id="vbGo" style="border-color:#7fe0c0;color:#8ff0d0">解锁</button><span id="vbMsg" style="font-size:10px;opacity:.75;margin-left:8px"></span></div>';
1376
+ In.innerHTML=h;
1377
+ document.getElementById('vbGo').onclick=function(){
1378
+ var p=(document.getElementById('vbPass')||{}).value||'';
1379
+ var key=vbKey(p,v.salt,v.iter||3000);
1380
+ try{vbMem={list:JSON.parse(vbDec(v.blob,key)),key:key};vbCache(key);vbRender();}
1381
+ catch(e){vbMsg('口令不对,再试试');}
1382
+ };
1383
+ return;
1384
+ }
1385
+ var L=(vbMem&&vbMem.list)||[],ed=vbEdit;
1386
+ h='<div style="font-size:11px;opacity:.85;margin-bottom:6px">🔐 本地密码本 <span style="opacity:.55;font-size:10px">只存本机 · '+L.length+' 条'+(v.lock?' · 已上锁':'')+'</span>';
1387
+ if(v.lock)h+='<button id="vbLock" style="float:right;font-size:10px;border-color:#ffd23f;color:#ffd66b">🔒 锁定</button>';
1388
+ h+='</div>';
1389
+ for(var i=0;i<L.length;i++){var e=L[i];
1390
+ h+='<div style="border:1px solid #1d3550;border-radius:3px;padding:6px 8px;margin-bottom:5px;background:#0a1220">'
1391
+ +'<div style="font-size:11px;color:#dff1ff"><b>'+vbEsc(e.n||'(未命名)')+'</b>'+(e.a?' · <span style="opacity:.55">'+vbEsc(e.a)+'</span>':'')+'</div>'
1392
+ +'<div style="font:11px ui-monospace,Menlo,monospace;color:#8ff0d0;margin-top:2px;word-break:break-all">'+vbEsc(e.p||'')+'</div>'
1393
+ +(e.r?'<div style="font-size:10px;opacity:.6;margin-top:2px">'+vbEsc(e.r)+'</div>':'')
1394
+ +'<div style="margin-top:4px"><button data-vbc="'+i+'" style="font-size:10px">复制</button> <button data-vbe="'+i+'" style="font-size:10px">编辑</button> <button data-vbr="'+i+'" style="font-size:10px;border-color:#ff5f57;color:#ff8a84">删除</button></div></div>';
1395
+ }
1396
+ if(!L.length)h+='<div style="font-size:10px;opacity:.6;margin-bottom:6px">还空着——从下面加第一条吧</div>';
1397
+ var E=(ed>=0&&L[ed])?L[ed]:{n:'',a:'',p:'',r:''};
1398
+ var IS='width:100%;box-sizing:border-box;background:#0b1424;border:1px solid #2b4a66;color:#dff1ff;border-radius:3px;padding:6px 8px;font:12px ui-monospace,Menlo,monospace;margin-top:4px';
1399
+ h+='<div style="border-top:1px dashed #2b4a66;padding-top:7px"><div style="font-size:10px;opacity:.65">'+(ed>=0?'编辑条目':'新增条目')+'</div>'
1400
+ +'<input id="vbN" value="'+vbEsc(E.n)+'" placeholder="名称(必填)· 例:DeepSeek API Key" style="'+IS+'">'
1401
+ +'<input id="vbA" value="'+vbEsc(E.a)+'" placeholder="账号(可空)" style="'+IS+'">'
1402
+ +'<input id="vbP" value="'+vbEsc(E.p)+'" placeholder="密码 / 密钥(必填)" style="'+IS+'">'
1403
+ +'<input id="vbR" value="'+vbEsc(E.r)+'" placeholder="备注(可空)" style="'+IS+'">'
1404
+ +'<div style="margin-top:6px"><button id="vbSv" style="border-color:#7fe0c0;color:#8ff0d0">'+(ed>=0?'保存修改':'+ 添加')+'</button>'+(ed>=0?' <button id="vbCn">取消编辑</button>':'')
1405
+ +'<span id="vbMsg" style="font-size:10px;opacity:.75;margin-left:8px"></span></div></div>';
1406
+ In.innerHTML=h;
1407
+ var lk=document.getElementById('vbLock');if(lk)lk.onclick=function(){vbCache(null);vbMem=null;vbRender();};
1408
+ for(var k2=0;k2<L.length;k2++){(function(idx){
1409
+ var c=In.querySelector('[data-vbc="'+idx+'"]');
1410
+ if(c)c.onclick=function(){vbMsg(vbCopy(L[idx].p||'')?'已复制 ✓':'复制失败,长按手动复制')};
1411
+ var eb2=In.querySelector('[data-vbe="'+idx+'"]');
1412
+ if(eb2)eb2.onclick=function(){vbEdit=idx;vbRender();};
1413
+ var rm=In.querySelector('[data-vbr="'+idx+'"]');
1414
+ if(rm)rm.onclick=function(){if(rm.getAttribute('data-arm')!=='1'){rm.setAttribute('data-arm','1');rm.textContent='再点=删除';return}L.splice(idx,1);vbPersist();vbEdit=-1;vbRender();};
1415
+ })(k2);}
1416
+ var sv=document.getElementById('vbSv');
1417
+ if(sv)sv.onclick=function(){
1418
+ var n=((document.getElementById('vbN')||{}).value||'').trim();
1419
+ var a=((document.getElementById('vbA')||{}).value||'').trim();
1420
+ var p=((document.getElementById('vbP')||{}).value||'').trim();
1421
+ var r=((document.getElementById('vbR')||{}).value||'').trim();
1422
+ if(!n){vbMsg('写个名称吧');return}
1423
+ if(!p){vbMsg('密码/密钥别忘了填');return}
1424
+ var it={n:n,a:a,p:p,r:r,ts:Date.now()};
1425
+ if(vbEdit>=0&&L[vbEdit])L[vbEdit]=it;else L.push(it);
1426
+ vbPersist();vbEdit=-1;vbMsg('已保存 ✓');
1427
+ setTimeout(function(){vbRender()},350);
1428
+ };
1429
+ var cn=document.getElementById('vbCn');
1430
+ if(cn)cn.onclick=function(){vbEdit=-1;vbRender();};
1431
+ }
1292
1432
  window.__shaT=sha;window.__pushT=push;window.__pullT=pull;window.__canonT=canon;window.__acct={admin:function(){return !!(acct&&acct.k===ADMIN_K)}};
1293
1433
  })();
1294
1434
  </script>
package/index.html CHANGED
@@ -542,7 +542,7 @@ img.av{filter:saturate(1.1) contrast(1.05) -webkit-user-drag:none}
542
542
  <div class="bnews" id="newsBar" data-act="news" style="cursor:pointer">
543
543
  <div class="hd">BREAKING NEWS</div>
544
544
  <div class="bx">
545
- <div style="font-size:.82em;line-height:1.4;font-weight:800;color:#ff5f57">版本 v1.2.1 正式上线!<br><span style="opacity:.75;font-size:.82em;font-weight:400;color:#ffd23f">点此查看使用教程与更新内容 ↗</span></div>
545
+ <div style="font-size:.82em;line-height:1.4;font-weight:800;color:#ff5f57">版本 v1.2.3 正式上线!<br><span style="opacity:.75;font-size:.82em;font-weight:400;color:#ffd23f">点此查看使用教程与更新内容 ↗</span></div>
546
546
  </div>
547
547
  </div>
548
548
 
@@ -1599,12 +1599,14 @@ function updTopStats(){
1599
1599
  var sn=document.getElementById('san-num');if(sn)sn.textContent=f;
1600
1600
  var ro=document.getElementById('res-or');
1601
1601
  if(ro){
1602
- var bal=REALBAL.base;
1602
+ var _ob=null;try{_ob=JSON.parse(localStorage.getItem('whale.bal.v1')||'null')}catch(e){}
1603
+ var bal=(_ob&&typeof _ob.v==='number')?_ob.v:REALBAL.base;
1604
+ var _bf=(_ob&&_ob.d)?_ob.d:REALBAL.from;
1603
1605
  try{
1604
1606
  var recs=JSON.parse(localStorage.getItem('minis_ledger_v1'))||[];
1605
1607
  for(var i=0;i<recs.length;i++){
1606
1608
  var r=recs[i];
1607
- if(r&&r.date&&r.date>REALBAL.from){bal+=(r.type==='in'?1:-1)*(Number(r.amount)||0);}
1609
+ if(r&&r.date&&r.date>_bf){bal+=(r.type==='in'?1:-1)*(Number(r.amount)||0);}
1608
1610
  }
1609
1611
  }catch(e){}
1610
1612
  ro.textContent='¥'+bal.toFixed(2);
@@ -1752,7 +1754,7 @@ function aiCall(cb){
1752
1754
  x.onload=function(){try{var j=JSON.parse(x.responseText);var t=j.choices&&j.choices[0]&&j.choices[0].message&&j.choices[0].message.content;if(t){cb(true,String(t).trim())}else{cb(false,'服务返回了意外内容(HTTP '+x.status+'):'+String(x.responseText||'').slice(0,110))}}catch(e){cb(false,'响应解析失败(HTTP '+x.status+')')}}
1753
1755
  x.onerror=function(){cb(false,'连接失败:请检查地址/Key,或该服务是否允许网页直连(CORS)')};
1754
1756
  x.ontimeout=function(){cb(false,'请求超时(90 秒)')};
1755
- x.send(JSON.stringify({model:c.m,messages:msgs,temperature:1}));
1757
+ var _bd={model:c.m,messages:msgs};_bd.temperature=(c.tp!==undefined&&c.tp!=='')?Number(c.tp):1;if(c.r)_bd.reasoning_effort=c.r;if(c.mt)_bd.max_tokens=parseInt(c.mt,10);x.send(JSON.stringify(_bd));
1756
1758
  }catch(e){cb(false,'发送异常:'+e.message)}
1757
1759
  }
1758
1760
  function aiSend(text){
@@ -1800,6 +1802,12 @@ V.chat=function(){
1800
1802
  '<input class="aiI" id="aiU" placeholder="API 地址,如 https://api.deepseek.com" value="'+(cfg?escH(cfg.u):'')+'">'+
1801
1803
  '<input class="aiI" id="aiK" placeholder="API Key(sk-...)" value="'+(cfg?escH(aiDec(cfg.k)):'')+'">'+
1802
1804
  '<input class="aiI" id="aiM" placeholder="模型名称,如 deepseek-flash / deepseek-v4-pro" value="'+(cfg?escH(cfg.m):'')+'">'+
1805
+ '<div style="display:flex;gap:6px;margin-top:8px">'+
1806
+ '<select id="aiR" class="aiI" style="flex:1.3"><option value=""'+(cfg&&cfg.r?'':' selected')+'>推理强度:自动</option><option value="low"'+(cfg&&cfg.r==='low'?' selected':'')+'>低</option><option value="medium"'+(cfg&&cfg.r==='medium'?' selected':'')+'>中</option><option value="high"'+(cfg&&cfg.r==='high'?' selected':'')+'>高</option></select>'+
1807
+ '<input class="aiI" id="aiTp" style="flex:1" placeholder="温度(默认1)" value="'+(cfg&&cfg.tp!=null?cfg.tp:'')+'">'+
1808
+ '<input class="aiI" id="aiMt" style="flex:1" placeholder="上限tokens" value="'+(cfg&&cfg.mt!=null?cfg.mt:'')+'">'+
1809
+ '</div>'+
1810
+ '<div style="font-size:.68em;opacity:.55;margin-top:6px">推理强度按 reasoning_effort 发送(服务商支持时才生效);温度 0~2;上限留空 = 默认不限</div>'+
1803
1811
  '<div style="margin-top:9px"><button class="btn" data-act="aisave">保存配置</button>'+(cfg?'<button class="btn sm" data-act="aiconf">收起</button>':'')+'</div>'+
1804
1812
  '<div style="font-size:.68em;opacity:.55;margin-top:7px">※ 该服务需允许网页直连(CORS)</div></div>';
1805
1813
  }
@@ -2633,7 +2641,7 @@ function tshuffle(a){for(var i=a.length-1;i>0;i--){var j=Math.floor(Math.random(
2633
2641
  var TARO_NOW=[];
2634
2642
  function openNews(){
2635
2643
  var h='<div class="box"><div class="tag rd">NOTICE</div>'+
2636
- '<div style="margin-top:6px;font-weight:900;font-size:1.08em">版本 v1.2.1 正式上线!</div>'+
2644
+ '<div style="margin-top:6px;font-weight:900;font-size:1.08em">版本 v1.2.3 正式上线!</div>'+
2637
2645
  '<div style="font-size:.78em;opacity:.7;margin-top:3px">鲸鲸的养成计划 · PRTS 终端 · 2026-09-27</div></div>'+
2638
2646
  '<div class="box" style="margin-top:10px"><div class="tag">使用教程</div>'+
2639
2647
  '<div style="font-size:.82em;line-height:1.8;margin-top:6px">'+
@@ -2643,7 +2651,11 @@ function openNews(){
2643
2651
  '④ 云存档:点 @ 登录 →「云端导入」接回数据 /「云端上传」备份上云;也有「本地存档码」跨设备搬运<br>'+
2644
2652
  '⑤ 恢复出厂:@ 面板内「清空本机存档」,一键清空本机数据(云端不受影响)<br>'+'⑥ AI 对话:01 里保存 API 配置(地址/Key/模型)→ 和「人格」开聊;采购中心的券可直接在对话里使用<br>'+'⑦ 意见箱:@ 面板里的 📮 直达云端,说什么都能被鲸鲸收到</div></div>'+
2645
2653
  '<div class="box" style="margin-top:10px"><div class="tag">免责声明</div><div style="font-size:.76em;line-height:1.75;margin-top:6px;opacity:.8">本站为个人学习与娱乐项目,无商业运营、无任何支付功能。AI 功能需自备 API 密钥,生成内容仅供参考;数据保存在你的设备与私有云存档中,可随时一键清空。</div></div>'+
2646
- '<div class="box" style="margin-top:10px"><div class="tag">更新内容 · v1.2.1</div>'+
2654
+ '<div class="box" style="margin-top:10px"><div class="tag">更新内容 · v1.2.3</div>'+
2655
+ '<div style="font-size:.82em;line-height:1.8;margin-top:6px">✦ 云端同步加固:开屏拉取 / 云端导入 / 登录校验 / 上传校验全部换用更稳的传输通道——弱网与受限环境也能把存档稳稳拉回来</div></div>'+
2656
+ '<div class="box" style="margin-top:10px"><div class="tag">里程碑 · v1.2.2</div>'+
2657
+ '<div style="font-size:.82em;line-height:1.8;margin-top:6px">✦ 本地密码本:@ 面板 🔐 · 只存本机、主口令加密、永不上云<br>✦ 收件箱:意见与战绩直达站内,主人专属(📬)<br>✦ AI 推理调节:对话页 ⚙ 新增 推理强度 / 温度 / 上限<br>✦ 账本余额校准:「💳 实际余额」自己填,主页余额同步跟动</div></div>'+
2658
+ '<div class="box" style="margin-top:10px"><div class="tag">里程碑 · v1.2.1</div>'+
2647
2659
  '<div style="font-size:.82em;line-height:1.8;margin-top:6px">✦ 意见箱上岗:@ 面板 📮 写信直达云端信箱,鲸鲸定期收信<br>✦ 一键报备:寻访战绩直传云端;和 AI 聊一句自动打卡<br>✦ API Key 加密保存:密文落盘、聊天照常<br>✦ 版本哨兵:新版上线自动提醒「点此刷新更新」<br>✦ 公开招募下线(九宫格重排 01-09)· 免责声明上站</div></div>'+
2648
2660
  '<div class="box" style="margin-top:10px"><div class="tag">里程碑 · v1.2.0</div>'+
2649
2661
  '<div style="font-size:.82em;line-height:1.8;margin-top:6px">✦ AI 对话 × 人格库:自助接入任意 OpenAI 兼容 API,聊天、看图两开花<br>✦ 图片上传:点 🖼 让 AI 看看你的世界(需视觉模型)<br>✦ 10 运势:塔罗升格为正式指令 + 专属「三牌归位」转场<br>✦ AI 记忆档案:称呼 + 认识与记忆,让它更懂你<br>✦ 每日补给:每逢 0 点自动 +30 小鱼干</div></div>'+
@@ -2791,7 +2803,12 @@ function handleAct(t){
2791
2803
  else if(a==='autofill'){S.squads[S.curSquad]=['makoto','aigis','mitsuru','akihiko','yukari','fuuka'];
2792
2804
  sfx.chime();toast('已按战力自动编成');V.squads()}
2793
2805
  else if(a==='aiconf'){aiFormOpen=aiFormOpen?0:1;sfx.tap();V.chat()}
2794
- else if(a==='aisave'){var _u=$('#aiU').value.trim(),_k=$('#aiK').value.trim(),_m=$('#aiM').value.trim();if(!_u||!_k||!_m){sfx.deny();toast('三项都要填哦');return}aiSave(AICFG_K,{u:_u,k:aiEnc(_k),m:_m});aiFormOpen=0;sfx.ok();toast('API 配置已保存');V.chat()}
2806
+ else if(a==='aisave'){var _u=$('#aiU').value.trim(),_k=$('#aiK').value.trim(),_m=$('#aiM').value.trim();if(!_u||!_k||!_m){sfx.deny();toast('三项都要填哦');return}
2807
+ var _r=$('#aiR')?$('#aiR').value:'',_tp=$('#aiTp')?$('#aiTp').value.trim():'',_mt=$('#aiMt')?$('#aiMt').value.trim():'';
2808
+ if(_tp!==''&&(isNaN(_tp)||Number(_tp)<0||Number(_tp)>2)){sfx.deny();toast('温度要在 0~2 之间');return}
2809
+ if(_mt!==''&&!/^\d+$/.test(_mt)){sfx.deny();toast('tokens 要填正整数');return}
2810
+ var _nc={u:_u,k:aiEnc(_k),m:_m};if(_r)_nc.r=_r;if(_tp!=='')_nc.tp=_tp;if(_mt!=='')_nc.mt=_mt;
2811
+ aiSave(AICFG_K,_nc);aiFormOpen=0;sfx.ok();toast('API 配置已保存');V.chat()}
2795
2812
  else if(a==='aisend'){var _v=($('#aiInp').value||'').trim();if(!_v&&!aiDraftImg){sfx.deny();return}$('#aiInp').value='';aiSend(_v)}
2796
2813
  else if(a==='aiclear'){var _p=aiActive(),_c=aiChats();_c[_p.id]=[];aiSave(AICHAT_K,_c);sfx.tap();toast('对话已清空');V.chat()}
2797
2814
  else if(a==='aicoupon'){try{if(TS&&TS.load)TS.load()}catch(e){}var _id=t.getAttribute('data-id'),_n=TS.d.own[_id]||0;if(_n<1){sfx.deny();toast('这张券用完了,去采购中心补货');return}TS.d.own[_id]=_n-1;TS.save();sfx.ok();var _nm='';TSHOP.forEach(function(g){if(g.id===_id)_nm=g.n});toast('已使用 · '+_nm);aiSend(AI_ACT[_id]||('(使用 '+_nm+')'))}
@@ -2976,7 +2993,7 @@ boot();
2976
2993
  (function(){
2977
2994
  var B=document.getElementById('petB'),bub=document.getElementById('petBub'),txt=document.getElementById('petTxt');
2978
2995
  if(!B)return;
2979
- var L=['鲸鲸~','主人~','捏到了!再捏一下嘛','今日宜抽卡','小鱼干 +1!','BUILD v1.2.1 已就位','账单同步了吗?','过场动画,好看吧?','晚安前记得说晚安哦','鲸鲸在看家…','要摸摸头吗?','别熬夜啦,主人'];
2996
+ var L=['鲸鲸~','主人~','捏到了!再捏一下嘛','今日宜抽卡','小鱼干 +1!','BUILD v1.2.3 已就位','账单同步了吗?','过场动画,好看吧?','晚安前记得说晚安哦','鲸鲸在看家…','要摸摸头吗?','别熬夜啦,主人'];
2980
2997
  var last=-1,ht=0;
2981
2998
  function say(){
2982
2999
  var i;do{i=Math.floor(Math.random()*L.length)}while(i===last);
@@ -3011,13 +3028,13 @@ boot();
3011
3028
  setInterval(function(){if(document.visibilityState==='visible')say()},36000);
3012
3029
  })();
3013
3030
  </script>
3014
- <div style="position:fixed;left:10px;bottom:5px;font-size:9px;letter-spacing:.14em;color:#4cc9ff;opacity:.5;z-index:45;font-family:ui-monospace,Menlo,monospace;pointer-events:none">BUILD v1.2.1</div>
3031
+ <div style="position:fixed;left:10px;bottom:5px;font-size:9px;letter-spacing:.14em;color:#4cc9ff;opacity:.5;z-index:45;font-family:ui-monospace,Menlo,monospace;pointer-events:none">BUILD v1.2.3</div>
3015
3032
 
3016
3033
  <!-- ===== SYSTEM LOGIN 云存档 ===== -->
3017
3034
  <script>
3018
3035
  /* ===== SYSTEM LOGIN · 云存档同步引擎 v1 ===== */
3019
3036
  (function(){
3020
- var TK=['whale.tasks.v1','minis_ledger_v1','minis_ledger_trash_v1','whale.bgm','whale.taro.v1','whale.ai.cfg.v1','whale.ai.psn.v1','whale.ai.chat.v1','whale.ai.memo.v1'];
3037
+ var TK=['whale.tasks.v1','minis_ledger_v1','minis_ledger_trash_v1','whale.bgm','whale.taro.v1','whale.ai.cfg.v1','whale.ai.psn.v1','whale.ai.chat.v1','whale.ai.memo.v1','whale.bal.v1'];
3021
3038
  var BASE='https://textdb.dev/api/data/prts-acct-';var ADMIN_K='e74aeb1a34c44f2fcefd1f19bb34c3891a4963f36242e5a5fd3869085892a711';
3022
3039
  var LS='whale.acct',LT='whale.sync.t';
3023
3040
  var acct=null;try{acct=JSON.parse(localStorage.getItem(LS)||'null')}catch(e){}
@@ -3051,6 +3068,33 @@ boot();
3051
3068
  }
3052
3069
  function snap(){var d={};for(var i=0;i<TK.length;i++){var v=localStorage.getItem(TK[i]);if(v!==null)d[TK[i]]=v;}return d;}
3053
3070
  function url(){return BASE+acct.k;}
3071
+ function mbFetch(cb){
3072
+ try{
3073
+ var x=new XMLHttpRequest();x.open('GET','https://textdb.dev/api/data/prts-myb-mailbox?ts='+Date.now(),true);x.timeout=20000;
3074
+ x.onload=function(){try{var cur=JSON.parse((''+x.responseText).replace(/=$/,''))||[];if(!(cur instanceof Array))cur=[];cb(cur)}catch(e){cb(null)}};
3075
+ x.onerror=function(){cb(null)};x.send();
3076
+ }catch(e){cb(null)}
3077
+ }
3078
+ function mbEsc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
3079
+ function mbRender(){
3080
+ var In=document.getElementById('mbIn');if(!In)return;
3081
+ if(!(window.__acct&&window.__acct.admin&&window.__acct.admin())){
3082
+ In.innerHTML='<div style="font-size:10px;opacity:.65">📬 收件箱 · 仅主人可见(先用你的账号登录再来看)</div>';return;
3083
+ }
3084
+ In.innerHTML='<div style="font-size:10px;opacity:.7">⏳ 收信中…</div>';
3085
+ mbFetch(function(list){
3086
+ var In2=document.getElementById('mbIn');if(!In2)return;
3087
+ if(list===null){In2.innerHTML='<div style="font-size:10px;opacity:.7">⚠ 收信失败 <button id="mbRe" style="font-size:10px">重试</button></div>';bindRe();return;}
3088
+ var h='<div style="font-size:11px;opacity:.85;margin-bottom:6px">📬 收件箱 <span style="opacity:.55;font-size:10px">共 '+list.length+' 封</span><button id="mbRe" style="float:right;font-size:10px">↻ 刷新</button></div>';
3089
+ for(var i=0;i<list.length;i++){var m=list[i]||{};var d=new Date(m.ts||0);
3090
+ var ts=('0'+(d.getMonth()+1)).slice(-2)+'-'+('0'+d.getDate()).slice(-2)+' '+('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2);
3091
+ h+='<div style="border:1px solid #1d3550;border-radius:3px;padding:6px 8px;margin-bottom:5px;background:#0a1220"><div style="font-size:10px;opacity:.75"><b style="color:'+(m.k==='战绩'?'#ffd66b':'#8ff0d0')+'">'+(m.k==='战绩'?'⚔ 战绩':'📮 意见')+'</b> · '+ts+'</div><div style="font-size:11px;color:#dff1ff;margin-top:3px;white-space:pre-wrap;word-break:break-word">'+mbEsc(m.t||'')+'</div></div>';
3092
+ }
3093
+ if(!list.length)h+='<div style="font-size:10px;opacity:.6">还没有来信~</div>';
3094
+ In2.innerHTML=h;bindRe();
3095
+ function bindRe(){var r3=document.getElementById('mbRe');if(r3)r3.onclick=function(){mbRender();};}
3096
+ });
3097
+ }
3054
3098
  function mbPost(kind,txt,cb){
3055
3099
  var u='https://textdb.dev/api/data/prts-myb-mailbox';
3056
3100
  try{
@@ -3089,17 +3133,17 @@ boot();
3089
3133
  wxhr(url(),JSON.stringify({v:1,u:acct.u,c:acct.c||0,t:t,data:snap()}),function(){st('✔ 已上传 · 后台校验…');setTimeout(function(){verify(t)},600);});
3090
3134
  }
3091
3135
  function verify(t){
3092
- fetch(url()+'?ts='+Date.now(),{cache:'no-store'}).then(function(r){return r.text()}).then(function(x){
3136
+ var x2=new XMLHttpRequest();x2.open('GET',url()+'?ts='+Date.now(),true);x2.timeout=20000;x2.onload=function(){try{var x=x2.responseText;
3093
3137
  x=(x||'').replace(/=$/,'');
3094
3138
  try{var rb=JSON.parse(x);if(rb.t===t){st('✔ 已同步 '+clock());return;}}catch(e){}
3095
3139
  if(!window.__pv){window.__pv=1;setTimeout(function(){push(1);},800);}
3096
3140
  else{window.__pv=0;st('⚠ 上传好像没送达(此浏览器限制跨域)——数据还在本机,可换 Safari 或用存档码');}
3097
- }).catch(function(){st('⚠ 校验失败(可能网络受限)');});
3141
+ }catch(e){st('⚠ 校验失败(可能网络受限)');}};x2.onerror=x2.ontimeout=function(){st('⚠ 校验失败(可能网络受限)');};x2.send();
3098
3142
  }
3099
3143
  function pull(manual){
3100
3144
  if(!acct||!acct.k)return;
3101
3145
  if(manual)st('↻ 同步中…');
3102
- fetch(url()+'?ts='+Date.now(),{cache:'no-store'}).then(function(r){return r.text()}).then(function(x){
3146
+ var x2=new XMLHttpRequest();x2.open('GET',url()+'?ts='+Date.now(),true);x2.timeout=20000;x2.onload=function(){try{var x=x2.responseText;
3103
3147
  x=(x||'').replace(/=$/,'');
3104
3148
  if(!x){if(manual)st('☁ 云端还没有存档——点「云端上传」把本机数据传上去');return;}
3105
3149
  var rb;try{rb=JSON.parse(x)}catch(e){st('⚠ 云端数据格式异常');return;}
@@ -3114,7 +3158,7 @@ boot();
3114
3158
  st(same2?'✔ 已是最新 '+clock():'☁ 云端比本机旧——点「云端上传」可覆盖云端');
3115
3159
  }
3116
3160
  else{st('✔ 已是云端最新');}
3117
- }).catch(function(){st('⚠ 云端连接失败');});
3161
+ }catch(e){st('⚠ 云端数据格式异常');}};x2.onerror=x2.ontimeout=function(){st('⚠ 云端连接失败');};x2.send();
3118
3162
  }
3119
3163
  function logout(){localStorage.removeItem(LS);acct=null;}
3120
3164
  var CSS='.acWrap{position:fixed;inset:0;z-index:9998;background:rgba(3,6,14,.66);display:none;align-items:center;justify-content:center}.acWrap.on{display:flex}'+
@@ -3142,11 +3186,15 @@ boot();
3142
3186
  }
3143
3187
  h+='<button id="acExp">💾 本地存档码</button><button id="acImp">📥 存档码导入</button><button id="acWipe" style="border-color:#ff5f57;color:#ff8a84">🧹 清空本机存档</button>'+
3144
3188
  '<button id="acFb" style="border-color:#ffd23f;color:#ffd66b">📮 意见箱</button>'+
3189
+ '<button id="vbBtn" style="border-color:#7fe0c0;color:#8ff0d0">🔐 本地密码本</button>'+
3190
+ (window.__acct&&window.__acct.admin&&window.__acct.admin()?'<button id="mbBtn" style="border-color:#9fd0ff;color:#c8e4ff">📬 收件箱</button>':'')+
3145
3191
  '<div class="st" id="acSt"></div><textarea id="acTa" spellcheck="false"></textarea>'+
3146
3192
  '<div id="fbBox" style="display:none;margin-top:10px;border-top:1px dashed #2b4a66;padding-top:9px">'+
3147
3193
  '<div style="font-size:11px;opacity:.78;margin-bottom:5px">📮 意见箱 · 内容直达云端信箱(可匿名),鲸鲸会定期收信</div>'+
3148
3194
  '<textarea id="fbTxt" spellcheck="false" style="display:block;height:64px" placeholder="想说的话、想要的更新、发现的 bug……"></textarea>'+
3149
3195
  '<div style="margin-top:6px"><button id="fbSend" style="border-color:#ffd23f;color:#ffd66b">寄到云端</button><span id="fbSt" style="font-size:10px;opacity:.75;margin-left:8px"></span></div></div>'+
3196
+ '<div id="vbBox" style="display:none;margin-top:10px;border-top:1px dashed #2b4a66;padding-top:9px"><div id="vbIn"></div></div>'+
3197
+ '<div id="mbBox" style="display:none;margin-top:10px;border-top:1px dashed #2b4a66;padding-top:9px"><div id="mbIn"></div></div>'+
3150
3198
  '<div style="font-size:10px;opacity:.5;margin-top:9px;line-height:1.5">密钥由「用户名+密码」派生;同一账号在任意设备登录即接回数据。忘记密码=新仓库(旧数据找不回)</div>'+
3151
3199
  '<button id="acX" style="border-color:#2b4a66;color:#8ea6bf;margin-top:10px">关闭 / CLOSE</button></div>';
3152
3200
  wrap.innerHTML=h;
@@ -3165,7 +3213,7 @@ boot();
3165
3213
  st('✦ 仓库已创建,正在上传本机数据…');render();push();return;
3166
3214
  }
3167
3215
  st('↻ 查找云端仓库…');
3168
- fetch(BASE+k+'?ts='+Date.now(),{cache:'no-store'}).then(function(r){return r.text()}).then(function(x){
3216
+ var x3=new XMLHttpRequest();x3.open('GET',BASE+k+'?ts='+Date.now(),true);x3.timeout=20000;x3.onload=function(){try{var x=x3.responseText;
3169
3217
  x=(x||'').replace(/=$/,'');
3170
3218
  if(x&&x.length>2){
3171
3219
  var rb=null;try{rb=JSON.parse(x)}catch(e){}if(!rb){acct=null;localStorage.removeItem(LS);st('⚠ 云端数据异常——没敢覆盖');return;}
@@ -3176,7 +3224,7 @@ boot();
3176
3224
  b.setAttribute('data-mode','create');b.textContent='✦ 没找到仓库——点此以本机数据创建';
3177
3225
  st('(如果是新账号,创建后本机数据会上传成初始存档)');
3178
3226
  }
3179
- }).catch(function(){st('⚠ 云端连接失败,稍后再试');});
3227
+ }catch(e){st('⚠ 云端连接失败,稍后再试');}};x3.onerror=x3.ontimeout=function(){st('⚠ 云端连接失败,稍后再试');};x3.send();
3180
3228
  };
3181
3229
  var pl=g('#acPull');if(pl)pl.onclick=function(){pull(1)};
3182
3230
  var pu=g('#acPush');if(pu)pu.onclick=function(){if(acct&&acct.k)push();};
@@ -3201,7 +3249,7 @@ boot();
3201
3249
  var wp=g('#acWipe');
3202
3250
  if(wp)wp.onclick=function(){
3203
3251
  if(wp.getAttribute('data-c')){
3204
- try{var ks=[],kk;for(var i=0;i<localStorage.length;i++){kk=localStorage.key(i);if(/^(whale\.|minis_ledger)/.test(kk))ks.push(kk);}
3252
+ try{var ks=[],kk;for(var i=0;i<localStorage.length;i++){kk=localStorage.key(i);if(/^(whale\.|minis_ledger)/.test(kk)&&kk.indexOf('whale.vault')!==0)ks.push(kk);}
3205
3253
  for(var j=0;j<ks.length;j++)localStorage.removeItem(ks[j]);}catch(e){}
3206
3254
  location.reload();return;}
3207
3255
  wp.setAttribute('data-c','1');wp.textContent='⚠ 再点一次=清空并刷新(云端不受影响)';};
@@ -3213,6 +3261,8 @@ boot();
3213
3261
  if(st2)st2.textContent='寄送中…';
3214
3262
  mbPost('意见',t2,function(ok){if(st2)st2.textContent=ok?'✅ 已寄出,谢谢!鲸鲸会看到的':'⚠ 寄送失败,稍后再试';if(ok&&t)t.value='';});
3215
3263
  };
3264
+ var mbb=g('#mbBtn');if(mbb)mbb.onclick=function(){var b=g('#mbBox');if(!b)return;var op=(!b.style.display||b.style.display==='none');b.style.display=op?'block':'none';if(op)mbRender();};
3265
+ var vbb=g('#vbBtn');if(vbb)vbb.onclick=function(){var b=g('#vbBox');if(!b)return;var op=(!b.style.display||b.style.display==='none');b.style.display=op?'block':'none';if(op){vbLoad();vbRender();}};
3216
3266
  var x=g('#acX');if(x)x.onclick=function(){wrap.classList.remove('on')};
3217
3267
  wrap.onclick=function(e){if(e.target===wrap)wrap.classList.remove('on')};
3218
3268
  }
@@ -3235,7 +3285,7 @@ boot();
3235
3285
  btn.addEventListener('click',open);
3236
3286
  setInterval(function(){if(acct&&acct.k){var c=canon();if(c!==lastSnap){lastSnap=c;clearTimeout(pq);pq=setTimeout(push,900);}}},2000);
3237
3287
  if(acct&&acct.k){setTimeout(function(){pull(false);},250);}
3238
- var APP_VER='1.2.1';
3288
+ var APP_VER='1.2.3';
3239
3289
  function _verCmp(a,b){var x=String(a).split('.'),y=String(b).split('.');for(var i=0;i<3;i++){var p=parseInt(x[i]||'0',10),q=parseInt(y[i]||'0',10);if(p!==q)return p-q}return 0}
3240
3290
  function chkVer(){
3241
3291
  try{
@@ -3257,6 +3307,113 @@ function chkVer(){
3257
3307
  }
3258
3308
  setTimeout(chkVer,2600);
3259
3309
  setInterval(chkVer,600000);
3310
+ /* ===== 🔐 本地密码本(whale.vault.v1 · 只存本机 · 不入同步/存档码/清空存档)===== */
3311
+ var VBK='whale.vault.v1',vbMem=null,vbEdit=-1;
3312
+ function vbRead(){try{return JSON.parse(localStorage.getItem(VBK)||'null')}catch(e){return null}}
3313
+ function vbSave(o){try{localStorage.setItem(VBK,JSON.stringify(o));return 1}catch(e){return 0}}
3314
+ function vbCache(s){try{if(s===null)sessionStorage.removeItem('whale.vault.un');else sessionStorage.setItem('whale.vault.un',s)}catch(e){}}
3315
+ function vbCached(){try{return sessionStorage.getItem('whale.vault.un')||''}catch(e){return ''}}
3316
+ function vbSalt(){var s='',h='0123456789abcdef';for(var i=0;i<16;i++)s+=h.charAt(Math.floor(Math.random()*16));return s;}
3317
+ function vbKey(pass,salt,iter){var x=salt+'|'+pass;for(var i=0;i<iter;i++)x=sha(x);return x;}
3318
+ function vbXor(s,key){var o='';for(var b=0;b<s.length;b+=32){var blk=sha(key+'|'+(b/32|0));
3319
+ for(var j=0;j<32&&b+j<s.length;j++)o+=String.fromCharCode(s.charCodeAt(b+j)^parseInt(blk.substr(j*2,2),16));}return o;}
3320
+ function vbEnc(t,key){return btoa(vbXor(unescape(encodeURIComponent(t)),key));}
3321
+ function vbDec(c,key){return decodeURIComponent(escape(vbXor(atob(c),key)));}
3322
+ function vbEsc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
3323
+ function vbMsg(t){var m=document.getElementById('vbMsg');if(m)m.textContent=t;}
3324
+ function vbCopy(t){var ok=0;try{navigator.clipboard.writeText(t);ok=1}catch(e){}
3325
+ if(!ok){try{var ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();ok=document.execCommand('copy');document.body.removeChild(ta)}catch(e2){}}
3326
+ return ok;}
3327
+ function vbLoad(){
3328
+ var v=vbRead();if(!v){return}
3329
+ if(!v.lock){vbMem={list:(v.list||[]),key:''};return}
3330
+ var ck=vbCached();if(!ck){vbMem=null;return}
3331
+ try{vbMem={list:JSON.parse(vbDec(v.blob,ck)),key:ck}}catch(e){vbCache(null);vbMem=null}
3332
+ }
3333
+ function vbPersist(){
3334
+ var v=vbRead();if(!v||!vbMem)return;
3335
+ if(v.lock){v.blob=vbEnc(JSON.stringify(vbMem.list),vbMem.key)}
3336
+ else{v.list=vbMem.list}
3337
+ vbSave(v);
3338
+ }
3339
+ function vbRender(){
3340
+ var In=document.getElementById('vbIn');if(!In)return;
3341
+ var v=vbRead(),h='';
3342
+ if(!v){
3343
+ h='<div style="font-size:11px;opacity:.85;margin-bottom:5px">🔐 本地密码本 · <span style="opacity:.55;font-size:10px">只存本机 · 永不云同步 · 不随存档导出</span></div>'
3344
+ +'<div style="font-size:10px;opacity:.6;margin-bottom:6px">第一次使用:设一个主口令(留空 = 不加锁)</div>'
3345
+ +'<input id="vbPass" type="password" placeholder="主口令(可留空)" style="width:100%;box-sizing:border-box;background:#0b1424;border:1px solid #2b4a66;color:#dff1ff;border-radius:3px;padding:7px 9px;font:12px ui-monospace,Menlo,monospace">'
3346
+ +'<div style="margin-top:6px"><button id="vbGo" style="border-color:#7fe0c0;color:#8ff0d0">创建密码本</button><span id="vbMsg" style="font-size:10px;opacity:.75;margin-left:8px"></span></div>';
3347
+ In.innerHTML=h;
3348
+ document.getElementById('vbGo').onclick=function(){
3349
+ var p=(document.getElementById('vbPass')||{}).value||'';
3350
+ if(!p){vbSave({v:1,lock:0,list:[]});vbMem={list:[],key:''};vbRender();return}
3351
+ var salt=vbSalt(),iter=3000,key=vbKey(p,salt,iter);
3352
+ vbSave({v:1,lock:1,salt:salt,iter:iter,blob:vbEnc('[]',key)});
3353
+ vbCache(key);vbMem={list:[],key:key};vbRender();
3354
+ };
3355
+ return;
3356
+ }
3357
+ if(v.lock&&!vbMem){
3358
+ h='<div style="font-size:11px;opacity:.85;margin-bottom:5px">🔐 本地密码本 · <b style="color:#ffd23f">已锁定</b></div>'
3359
+ +'<input id="vbPass" type="password" placeholder="主口令" style="width:100%;box-sizing:border-box;background:#0b1424;border:1px solid #2b4a66;color:#dff1ff;border-radius:3px;padding:7px 9px;font:12px ui-monospace,Menlo,monospace">'
3360
+ +'<div style="margin-top:6px"><button id="vbGo" style="border-color:#7fe0c0;color:#8ff0d0">解锁</button><span id="vbMsg" style="font-size:10px;opacity:.75;margin-left:8px"></span></div>';
3361
+ In.innerHTML=h;
3362
+ document.getElementById('vbGo').onclick=function(){
3363
+ var p=(document.getElementById('vbPass')||{}).value||'';
3364
+ var key=vbKey(p,v.salt,v.iter||3000);
3365
+ try{vbMem={list:JSON.parse(vbDec(v.blob,key)),key:key};vbCache(key);vbRender();}
3366
+ catch(e){vbMsg('口令不对,再试试');}
3367
+ };
3368
+ return;
3369
+ }
3370
+ var L=(vbMem&&vbMem.list)||[],ed=vbEdit;
3371
+ h='<div style="font-size:11px;opacity:.85;margin-bottom:6px">🔐 本地密码本 <span style="opacity:.55;font-size:10px">只存本机 · '+L.length+' 条'+(v.lock?' · 已上锁':'')+'</span>';
3372
+ if(v.lock)h+='<button id="vbLock" style="float:right;font-size:10px;border-color:#ffd23f;color:#ffd66b">🔒 锁定</button>';
3373
+ h+='</div>';
3374
+ for(var i=0;i<L.length;i++){var e=L[i];
3375
+ h+='<div style="border:1px solid #1d3550;border-radius:3px;padding:6px 8px;margin-bottom:5px;background:#0a1220">'
3376
+ +'<div style="font-size:11px;color:#dff1ff"><b>'+vbEsc(e.n||'(未命名)')+'</b>'+(e.a?' · <span style="opacity:.55">'+vbEsc(e.a)+'</span>':'')+'</div>'
3377
+ +'<div style="font:11px ui-monospace,Menlo,monospace;color:#8ff0d0;margin-top:2px;word-break:break-all">'+vbEsc(e.p||'')+'</div>'
3378
+ +(e.r?'<div style="font-size:10px;opacity:.6;margin-top:2px">'+vbEsc(e.r)+'</div>':'')
3379
+ +'<div style="margin-top:4px"><button data-vbc="'+i+'" style="font-size:10px">复制</button> <button data-vbe="'+i+'" style="font-size:10px">编辑</button> <button data-vbr="'+i+'" style="font-size:10px;border-color:#ff5f57;color:#ff8a84">删除</button></div></div>';
3380
+ }
3381
+ if(!L.length)h+='<div style="font-size:10px;opacity:.6;margin-bottom:6px">还空着——从下面加第一条吧</div>';
3382
+ var E=(ed>=0&&L[ed])?L[ed]:{n:'',a:'',p:'',r:''};
3383
+ var IS='width:100%;box-sizing:border-box;background:#0b1424;border:1px solid #2b4a66;color:#dff1ff;border-radius:3px;padding:6px 8px;font:12px ui-monospace,Menlo,monospace;margin-top:4px';
3384
+ h+='<div style="border-top:1px dashed #2b4a66;padding-top:7px"><div style="font-size:10px;opacity:.65">'+(ed>=0?'编辑条目':'新增条目')+'</div>'
3385
+ +'<input id="vbN" value="'+vbEsc(E.n)+'" placeholder="名称(必填)· 例:DeepSeek API Key" style="'+IS+'">'
3386
+ +'<input id="vbA" value="'+vbEsc(E.a)+'" placeholder="账号(可空)" style="'+IS+'">'
3387
+ +'<input id="vbP" value="'+vbEsc(E.p)+'" placeholder="密码 / 密钥(必填)" style="'+IS+'">'
3388
+ +'<input id="vbR" value="'+vbEsc(E.r)+'" placeholder="备注(可空)" style="'+IS+'">'
3389
+ +'<div style="margin-top:6px"><button id="vbSv" style="border-color:#7fe0c0;color:#8ff0d0">'+(ed>=0?'保存修改':'+ 添加')+'</button>'+(ed>=0?' <button id="vbCn">取消编辑</button>':'')
3390
+ +'<span id="vbMsg" style="font-size:10px;opacity:.75;margin-left:8px"></span></div></div>';
3391
+ In.innerHTML=h;
3392
+ var lk=document.getElementById('vbLock');if(lk)lk.onclick=function(){vbCache(null);vbMem=null;vbRender();};
3393
+ for(var k2=0;k2<L.length;k2++){(function(idx){
3394
+ var c=In.querySelector('[data-vbc="'+idx+'"]');
3395
+ if(c)c.onclick=function(){vbMsg(vbCopy(L[idx].p||'')?'已复制 ✓':'复制失败,长按手动复制')};
3396
+ var eb2=In.querySelector('[data-vbe="'+idx+'"]');
3397
+ if(eb2)eb2.onclick=function(){vbEdit=idx;vbRender();};
3398
+ var rm=In.querySelector('[data-vbr="'+idx+'"]');
3399
+ if(rm)rm.onclick=function(){if(rm.getAttribute('data-arm')!=='1'){rm.setAttribute('data-arm','1');rm.textContent='再点=删除';return}L.splice(idx,1);vbPersist();vbEdit=-1;vbRender();};
3400
+ })(k2);}
3401
+ var sv=document.getElementById('vbSv');
3402
+ if(sv)sv.onclick=function(){
3403
+ var n=((document.getElementById('vbN')||{}).value||'').trim();
3404
+ var a=((document.getElementById('vbA')||{}).value||'').trim();
3405
+ var p=((document.getElementById('vbP')||{}).value||'').trim();
3406
+ var r=((document.getElementById('vbR')||{}).value||'').trim();
3407
+ if(!n){vbMsg('写个名称吧');return}
3408
+ if(!p){vbMsg('密码/密钥别忘了填');return}
3409
+ var it={n:n,a:a,p:p,r:r,ts:Date.now()};
3410
+ if(vbEdit>=0&&L[vbEdit])L[vbEdit]=it;else L.push(it);
3411
+ vbPersist();vbEdit=-1;vbMsg('已保存 ✓');
3412
+ setTimeout(function(){vbRender()},350);
3413
+ };
3414
+ var cn=document.getElementById('vbCn');
3415
+ if(cn)cn.onclick=function(){vbEdit=-1;vbRender();};
3416
+ }
3260
3417
  window.__shaT=sha;window.__pushT=push;window.__pullT=pull;window.__canonT=canon;window.__acct={admin:function(){return !!(acct&&acct.k===ADMIN_K)}};
3261
3418
  })();
3262
3419
  </script>
package/ledger.html CHANGED
@@ -261,6 +261,7 @@ input.rdate::-webkit-date-and-time-value{text-align:right}
261
261
  <div><span class="k">收入</span><b id="inTxt">¥0.00</b></div>
262
262
  <div><span class="k">笔数</span><b id="cntTxt">0</b></div>
263
263
  </div>
264
+ <div class="sum-row" id="realRow" style="margin-top:4px"><div style="flex:1"><span class="k">💳 实际余额</span><b id="realBalTxt">¥0.00</b></div></div>
264
265
  </section>
265
266
 
266
267
  <div class="card">
@@ -663,7 +664,35 @@ window.LEDGER_BACKUP = [];
663
664
  box.innerHTML = html;
664
665
  }
665
666
 
666
- function renderAll() { renderHero(); renderList(); renderStat(); }
667
+ /* ---------- 💳 实际余额(手动校准 · whale.bal.v1) ---------- */
668
+ function balRaw() { try { return JSON.parse(localStorage.getItem('whale.bal.v1') || 'null'); } catch (e) { return null; } }
669
+ function balCalc() {
670
+ var ob = balRaw(), v = (ob && typeof ob.v === 'number') ? ob.v : 0, d = (ob && ob.d) ? ob.d : '2026-09-27';
671
+ (S.records || []).forEach(function (r) { if (r && r.date && r.date > d) { v += (r.type === 'in' ? 1 : -1) * (Number(r.amount) || 0); } });
672
+ return v;
673
+ }
674
+ var realEdit = false;
675
+ function renderReal() {
676
+ var w = $('realRow'); if (!w) return;
677
+ if (realEdit && document.getElementById('realBalIn')) return;
678
+ var ob = balRaw();
679
+ if (!realEdit) {
680
+ w.innerHTML = '<div style="flex:1"><span class="k">💳 实际余额</span><b id="realBalTxt">¥' + money(balCalc()) + '</b> <button id="realBalEd" style="margin-left:8px;font-size:11px;padding:2px 9px">校准</button><span style="font-size:10px;opacity:.5;margin-left:6px">' + (ob && ob.d ? ('校准于 ' + ob.d) : '未校准(点「校准」填写)') + '</span></div>';
681
+ $('realBalEd').onclick = function () { realEdit = true; renderReal(); };
682
+ } else {
683
+ var cur = (ob && typeof ob.v === 'number') ? ob.v : 0;
684
+ w.innerHTML = '<div style="flex:1"><span class="k">💳 实际余额</span> <input id="realBalIn" type="number" step="0.01" value="' + cur + '" style="width:110px;font-size:13px;padding:3px 6px"> <button id="realBalSv" style="font-size:11px;padding:2px 9px">保存</button> <button id="realBalCn" style="font-size:11px;padding:2px 9px">取消</button><span style="font-size:10px;opacity:.5;margin-left:6px">填你现在的真实余额</span></div>';
685
+ $('realBalSv').onclick = function () {
686
+ var x = Number($('realBalIn').value);
687
+ if (!isFinite(x)) return;
688
+ x = Math.round(x * 100) / 100;
689
+ try { localStorage.setItem('whale.bal.v1', JSON.stringify({ v: x, d: todayStr() })); } catch (e) {}
690
+ realEdit = false; renderReal();
691
+ };
692
+ $('realBalCn').onclick = function () { realEdit = false; renderReal(); };
693
+ }
694
+ }
695
+ function renderAll() { renderHero(); renderList(); renderStat(); renderReal(); }
667
696
 
668
697
  /* ---------- 事件 ---------- */
669
698
  function bindTabs() {
@@ -1009,7 +1038,7 @@ window.LEDGER_BACKUP = [];
1009
1038
  <script>
1010
1039
  /* ===== SYSTEM LOGIN · 云存档同步引擎 v1 ===== */
1011
1040
  (function(){
1012
- var TK=['whale.tasks.v1','minis_ledger_v1','minis_ledger_trash_v1','whale.bgm','whale.taro.v1','whale.ai.cfg.v1','whale.ai.psn.v1','whale.ai.chat.v1','whale.ai.memo.v1'];
1041
+ var TK=['whale.tasks.v1','minis_ledger_v1','minis_ledger_trash_v1','whale.bgm','whale.taro.v1','whale.ai.cfg.v1','whale.ai.psn.v1','whale.ai.chat.v1','whale.ai.memo.v1','whale.bal.v1'];
1013
1042
  var BASE='https://textdb.dev/api/data/prts-acct-';var ADMIN_K='e74aeb1a34c44f2fcefd1f19bb34c3891a4963f36242e5a5fd3869085892a711';
1014
1043
  var LS='whale.acct',LT='whale.sync.t';
1015
1044
  var acct=null;try{acct=JSON.parse(localStorage.getItem(LS)||'null')}catch(e){}
@@ -1043,6 +1072,33 @@ window.LEDGER_BACKUP = [];
1043
1072
  }
1044
1073
  function snap(){var d={};for(var i=0;i<TK.length;i++){var v=localStorage.getItem(TK[i]);if(v!==null)d[TK[i]]=v;}return d;}
1045
1074
  function url(){return BASE+acct.k;}
1075
+ function mbFetch(cb){
1076
+ try{
1077
+ var x=new XMLHttpRequest();x.open('GET','https://textdb.dev/api/data/prts-myb-mailbox?ts='+Date.now(),true);x.timeout=20000;
1078
+ x.onload=function(){try{var cur=JSON.parse((''+x.responseText).replace(/=$/,''))||[];if(!(cur instanceof Array))cur=[];cb(cur)}catch(e){cb(null)}};
1079
+ x.onerror=function(){cb(null)};x.send();
1080
+ }catch(e){cb(null)}
1081
+ }
1082
+ function mbEsc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
1083
+ function mbRender(){
1084
+ var In=document.getElementById('mbIn');if(!In)return;
1085
+ if(!(window.__acct&&window.__acct.admin&&window.__acct.admin())){
1086
+ In.innerHTML='<div style="font-size:10px;opacity:.65">📬 收件箱 · 仅主人可见(先用你的账号登录再来看)</div>';return;
1087
+ }
1088
+ In.innerHTML='<div style="font-size:10px;opacity:.7">⏳ 收信中…</div>';
1089
+ mbFetch(function(list){
1090
+ var In2=document.getElementById('mbIn');if(!In2)return;
1091
+ if(list===null){In2.innerHTML='<div style="font-size:10px;opacity:.7">⚠ 收信失败 <button id="mbRe" style="font-size:10px">重试</button></div>';bindRe();return;}
1092
+ var h='<div style="font-size:11px;opacity:.85;margin-bottom:6px">📬 收件箱 <span style="opacity:.55;font-size:10px">共 '+list.length+' 封</span><button id="mbRe" style="float:right;font-size:10px">↻ 刷新</button></div>';
1093
+ for(var i=0;i<list.length;i++){var m=list[i]||{};var d=new Date(m.ts||0);
1094
+ var ts=('0'+(d.getMonth()+1)).slice(-2)+'-'+('0'+d.getDate()).slice(-2)+' '+('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2);
1095
+ h+='<div style="border:1px solid #1d3550;border-radius:3px;padding:6px 8px;margin-bottom:5px;background:#0a1220"><div style="font-size:10px;opacity:.75"><b style="color:'+(m.k==='战绩'?'#ffd66b':'#8ff0d0')+'">'+(m.k==='战绩'?'⚔ 战绩':'📮 意见')+'</b> · '+ts+'</div><div style="font-size:11px;color:#dff1ff;margin-top:3px;white-space:pre-wrap;word-break:break-word">'+mbEsc(m.t||'')+'</div></div>';
1096
+ }
1097
+ if(!list.length)h+='<div style="font-size:10px;opacity:.6">还没有来信~</div>';
1098
+ In2.innerHTML=h;bindRe();
1099
+ function bindRe(){var r3=document.getElementById('mbRe');if(r3)r3.onclick=function(){mbRender();};}
1100
+ });
1101
+ }
1046
1102
  function mbPost(kind,txt,cb){
1047
1103
  var u='https://textdb.dev/api/data/prts-myb-mailbox';
1048
1104
  try{
@@ -1081,17 +1137,17 @@ window.LEDGER_BACKUP = [];
1081
1137
  wxhr(url(),JSON.stringify({v:1,u:acct.u,c:acct.c||0,t:t,data:snap()}),function(){st('✔ 已上传 · 后台校验…');setTimeout(function(){verify(t)},600);});
1082
1138
  }
1083
1139
  function verify(t){
1084
- fetch(url()+'?ts='+Date.now(),{cache:'no-store'}).then(function(r){return r.text()}).then(function(x){
1140
+ var x2=new XMLHttpRequest();x2.open('GET',url()+'?ts='+Date.now(),true);x2.timeout=20000;x2.onload=function(){try{var x=x2.responseText;
1085
1141
  x=(x||'').replace(/=$/,'');
1086
1142
  try{var rb=JSON.parse(x);if(rb.t===t){st('✔ 已同步 '+clock());return;}}catch(e){}
1087
1143
  if(!window.__pv){window.__pv=1;setTimeout(function(){push(1);},800);}
1088
1144
  else{window.__pv=0;st('⚠ 上传好像没送达(此浏览器限制跨域)——数据还在本机,可换 Safari 或用存档码');}
1089
- }).catch(function(){st('⚠ 校验失败(可能网络受限)');});
1145
+ }catch(e){st('⚠ 校验失败(可能网络受限)');}};x2.onerror=x2.ontimeout=function(){st('⚠ 校验失败(可能网络受限)');};x2.send();
1090
1146
  }
1091
1147
  function pull(manual){
1092
1148
  if(!acct||!acct.k)return;
1093
1149
  if(manual)st('↻ 同步中…');
1094
- fetch(url()+'?ts='+Date.now(),{cache:'no-store'}).then(function(r){return r.text()}).then(function(x){
1150
+ var x2=new XMLHttpRequest();x2.open('GET',url()+'?ts='+Date.now(),true);x2.timeout=20000;x2.onload=function(){try{var x=x2.responseText;
1095
1151
  x=(x||'').replace(/=$/,'');
1096
1152
  if(!x){if(manual)st('☁ 云端还没有存档——点「云端上传」把本机数据传上去');return;}
1097
1153
  var rb;try{rb=JSON.parse(x)}catch(e){st('⚠ 云端数据格式异常');return;}
@@ -1106,7 +1162,7 @@ window.LEDGER_BACKUP = [];
1106
1162
  st(same2?'✔ 已是最新 '+clock():'☁ 云端比本机旧——点「云端上传」可覆盖云端');
1107
1163
  }
1108
1164
  else{st('✔ 已是云端最新');}
1109
- }).catch(function(){st('⚠ 云端连接失败');});
1165
+ }catch(e){st('⚠ 云端数据格式异常');}};x2.onerror=x2.ontimeout=function(){st('⚠ 云端连接失败');};x2.send();
1110
1166
  }
1111
1167
  function logout(){localStorage.removeItem(LS);acct=null;}
1112
1168
  var CSS='.acWrap{position:fixed;inset:0;z-index:9998;background:rgba(3,6,14,.66);display:none;align-items:center;justify-content:center}.acWrap.on{display:flex}'+
@@ -1134,11 +1190,15 @@ window.LEDGER_BACKUP = [];
1134
1190
  }
1135
1191
  h+='<button id="acExp">💾 本地存档码</button><button id="acImp">📥 存档码导入</button><button id="acWipe" style="border-color:#ff5f57;color:#ff8a84">🧹 清空本机存档</button>'+
1136
1192
  '<button id="acFb" style="border-color:#ffd23f;color:#ffd66b">📮 意见箱</button>'+
1193
+ '<button id="vbBtn" style="border-color:#7fe0c0;color:#8ff0d0">🔐 本地密码本</button>'+
1194
+ (window.__acct&&window.__acct.admin&&window.__acct.admin()?'<button id="mbBtn" style="border-color:#9fd0ff;color:#c8e4ff">📬 收件箱</button>':'')+
1137
1195
  '<div class="st" id="acSt"></div><textarea id="acTa" spellcheck="false"></textarea>'+
1138
1196
  '<div id="fbBox" style="display:none;margin-top:10px;border-top:1px dashed #2b4a66;padding-top:9px">'+
1139
1197
  '<div style="font-size:11px;opacity:.78;margin-bottom:5px">📮 意见箱 · 内容直达云端信箱(可匿名),鲸鲸会定期收信</div>'+
1140
1198
  '<textarea id="fbTxt" spellcheck="false" style="display:block;height:64px" placeholder="想说的话、想要的更新、发现的 bug……"></textarea>'+
1141
1199
  '<div style="margin-top:6px"><button id="fbSend" style="border-color:#ffd23f;color:#ffd66b">寄到云端</button><span id="fbSt" style="font-size:10px;opacity:.75;margin-left:8px"></span></div></div>'+
1200
+ '<div id="vbBox" style="display:none;margin-top:10px;border-top:1px dashed #2b4a66;padding-top:9px"><div id="vbIn"></div></div>'+
1201
+ '<div id="mbBox" style="display:none;margin-top:10px;border-top:1px dashed #2b4a66;padding-top:9px"><div id="mbIn"></div></div>'+
1142
1202
  '<div style="font-size:10px;opacity:.5;margin-top:9px;line-height:1.5">密钥由「用户名+密码」派生;同一账号在任意设备登录即接回数据。忘记密码=新仓库(旧数据找不回)</div>'+
1143
1203
  '<button id="acX" style="border-color:#2b4a66;color:#8ea6bf;margin-top:10px">关闭 / CLOSE</button></div>';
1144
1204
  wrap.innerHTML=h;
@@ -1157,7 +1217,7 @@ window.LEDGER_BACKUP = [];
1157
1217
  st('✦ 仓库已创建,正在上传本机数据…');render();push();return;
1158
1218
  }
1159
1219
  st('↻ 查找云端仓库…');
1160
- fetch(BASE+k+'?ts='+Date.now(),{cache:'no-store'}).then(function(r){return r.text()}).then(function(x){
1220
+ var x3=new XMLHttpRequest();x3.open('GET',BASE+k+'?ts='+Date.now(),true);x3.timeout=20000;x3.onload=function(){try{var x=x3.responseText;
1161
1221
  x=(x||'').replace(/=$/,'');
1162
1222
  if(x&&x.length>2){
1163
1223
  var rb=null;try{rb=JSON.parse(x)}catch(e){}if(!rb){acct=null;localStorage.removeItem(LS);st('⚠ 云端数据异常——没敢覆盖');return;}
@@ -1168,7 +1228,7 @@ window.LEDGER_BACKUP = [];
1168
1228
  b.setAttribute('data-mode','create');b.textContent='✦ 没找到仓库——点此以本机数据创建';
1169
1229
  st('(如果是新账号,创建后本机数据会上传成初始存档)');
1170
1230
  }
1171
- }).catch(function(){st('⚠ 云端连接失败,稍后再试');});
1231
+ }catch(e){st('⚠ 云端连接失败,稍后再试');}};x3.onerror=x3.ontimeout=function(){st('⚠ 云端连接失败,稍后再试');};x3.send();
1172
1232
  };
1173
1233
  var pl=g('#acPull');if(pl)pl.onclick=function(){pull(1)};
1174
1234
  var pu=g('#acPush');if(pu)pu.onclick=function(){if(acct&&acct.k)push();};
@@ -1193,7 +1253,7 @@ window.LEDGER_BACKUP = [];
1193
1253
  var wp=g('#acWipe');
1194
1254
  if(wp)wp.onclick=function(){
1195
1255
  if(wp.getAttribute('data-c')){
1196
- try{var ks=[],kk;for(var i=0;i<localStorage.length;i++){kk=localStorage.key(i);if(/^(whale\.|minis_ledger)/.test(kk))ks.push(kk);}
1256
+ try{var ks=[],kk;for(var i=0;i<localStorage.length;i++){kk=localStorage.key(i);if(/^(whale\.|minis_ledger)/.test(kk)&&kk.indexOf('whale.vault')!==0)ks.push(kk);}
1197
1257
  for(var j=0;j<ks.length;j++)localStorage.removeItem(ks[j]);}catch(e){}
1198
1258
  location.reload();return;}
1199
1259
  wp.setAttribute('data-c','1');wp.textContent='⚠ 再点一次=清空并刷新(云端不受影响)';};
@@ -1205,6 +1265,8 @@ window.LEDGER_BACKUP = [];
1205
1265
  if(st2)st2.textContent='寄送中…';
1206
1266
  mbPost('意见',t2,function(ok){if(st2)st2.textContent=ok?'✅ 已寄出,谢谢!鲸鲸会看到的':'⚠ 寄送失败,稍后再试';if(ok&&t)t.value='';});
1207
1267
  };
1268
+ var mbb=g('#mbBtn');if(mbb)mbb.onclick=function(){var b=g('#mbBox');if(!b)return;var op=(!b.style.display||b.style.display==='none');b.style.display=op?'block':'none';if(op)mbRender();};
1269
+ var vbb=g('#vbBtn');if(vbb)vbb.onclick=function(){var b=g('#vbBox');if(!b)return;var op=(!b.style.display||b.style.display==='none');b.style.display=op?'block':'none';if(op){vbLoad();vbRender();}};
1208
1270
  var x=g('#acX');if(x)x.onclick=function(){wrap.classList.remove('on')};
1209
1271
  wrap.onclick=function(e){if(e.target===wrap)wrap.classList.remove('on')};
1210
1272
  }
@@ -1227,7 +1289,7 @@ window.LEDGER_BACKUP = [];
1227
1289
  btn.addEventListener('click',open);
1228
1290
  setInterval(function(){if(acct&&acct.k){var c=canon();if(c!==lastSnap){lastSnap=c;clearTimeout(pq);pq=setTimeout(push,900);}}},2000);
1229
1291
  if(acct&&acct.k){setTimeout(function(){pull(false);},250);}
1230
- var APP_VER='1.2.1';
1292
+ var APP_VER='1.2.3';
1231
1293
  function _verCmp(a,b){var x=String(a).split('.'),y=String(b).split('.');for(var i=0;i<3;i++){var p=parseInt(x[i]||'0',10),q=parseInt(y[i]||'0',10);if(p!==q)return p-q}return 0}
1232
1294
  function chkVer(){
1233
1295
  try{
@@ -1249,6 +1311,113 @@ function chkVer(){
1249
1311
  }
1250
1312
  setTimeout(chkVer,2600);
1251
1313
  setInterval(chkVer,600000);
1314
+ /* ===== 🔐 本地密码本(whale.vault.v1 · 只存本机 · 不入同步/存档码/清空存档)===== */
1315
+ var VBK='whale.vault.v1',vbMem=null,vbEdit=-1;
1316
+ function vbRead(){try{return JSON.parse(localStorage.getItem(VBK)||'null')}catch(e){return null}}
1317
+ function vbSave(o){try{localStorage.setItem(VBK,JSON.stringify(o));return 1}catch(e){return 0}}
1318
+ function vbCache(s){try{if(s===null)sessionStorage.removeItem('whale.vault.un');else sessionStorage.setItem('whale.vault.un',s)}catch(e){}}
1319
+ function vbCached(){try{return sessionStorage.getItem('whale.vault.un')||''}catch(e){return ''}}
1320
+ function vbSalt(){var s='',h='0123456789abcdef';for(var i=0;i<16;i++)s+=h.charAt(Math.floor(Math.random()*16));return s;}
1321
+ function vbKey(pass,salt,iter){var x=salt+'|'+pass;for(var i=0;i<iter;i++)x=sha(x);return x;}
1322
+ function vbXor(s,key){var o='';for(var b=0;b<s.length;b+=32){var blk=sha(key+'|'+(b/32|0));
1323
+ for(var j=0;j<32&&b+j<s.length;j++)o+=String.fromCharCode(s.charCodeAt(b+j)^parseInt(blk.substr(j*2,2),16));}return o;}
1324
+ function vbEnc(t,key){return btoa(vbXor(unescape(encodeURIComponent(t)),key));}
1325
+ function vbDec(c,key){return decodeURIComponent(escape(vbXor(atob(c),key)));}
1326
+ function vbEsc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
1327
+ function vbMsg(t){var m=document.getElementById('vbMsg');if(m)m.textContent=t;}
1328
+ function vbCopy(t){var ok=0;try{navigator.clipboard.writeText(t);ok=1}catch(e){}
1329
+ if(!ok){try{var ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();ok=document.execCommand('copy');document.body.removeChild(ta)}catch(e2){}}
1330
+ return ok;}
1331
+ function vbLoad(){
1332
+ var v=vbRead();if(!v){return}
1333
+ if(!v.lock){vbMem={list:(v.list||[]),key:''};return}
1334
+ var ck=vbCached();if(!ck){vbMem=null;return}
1335
+ try{vbMem={list:JSON.parse(vbDec(v.blob,ck)),key:ck}}catch(e){vbCache(null);vbMem=null}
1336
+ }
1337
+ function vbPersist(){
1338
+ var v=vbRead();if(!v||!vbMem)return;
1339
+ if(v.lock){v.blob=vbEnc(JSON.stringify(vbMem.list),vbMem.key)}
1340
+ else{v.list=vbMem.list}
1341
+ vbSave(v);
1342
+ }
1343
+ function vbRender(){
1344
+ var In=document.getElementById('vbIn');if(!In)return;
1345
+ var v=vbRead(),h='';
1346
+ if(!v){
1347
+ h='<div style="font-size:11px;opacity:.85;margin-bottom:5px">🔐 本地密码本 · <span style="opacity:.55;font-size:10px">只存本机 · 永不云同步 · 不随存档导出</span></div>'
1348
+ +'<div style="font-size:10px;opacity:.6;margin-bottom:6px">第一次使用:设一个主口令(留空 = 不加锁)</div>'
1349
+ +'<input id="vbPass" type="password" placeholder="主口令(可留空)" style="width:100%;box-sizing:border-box;background:#0b1424;border:1px solid #2b4a66;color:#dff1ff;border-radius:3px;padding:7px 9px;font:12px ui-monospace,Menlo,monospace">'
1350
+ +'<div style="margin-top:6px"><button id="vbGo" style="border-color:#7fe0c0;color:#8ff0d0">创建密码本</button><span id="vbMsg" style="font-size:10px;opacity:.75;margin-left:8px"></span></div>';
1351
+ In.innerHTML=h;
1352
+ document.getElementById('vbGo').onclick=function(){
1353
+ var p=(document.getElementById('vbPass')||{}).value||'';
1354
+ if(!p){vbSave({v:1,lock:0,list:[]});vbMem={list:[],key:''};vbRender();return}
1355
+ var salt=vbSalt(),iter=3000,key=vbKey(p,salt,iter);
1356
+ vbSave({v:1,lock:1,salt:salt,iter:iter,blob:vbEnc('[]',key)});
1357
+ vbCache(key);vbMem={list:[],key:key};vbRender();
1358
+ };
1359
+ return;
1360
+ }
1361
+ if(v.lock&&!vbMem){
1362
+ h='<div style="font-size:11px;opacity:.85;margin-bottom:5px">🔐 本地密码本 · <b style="color:#ffd23f">已锁定</b></div>'
1363
+ +'<input id="vbPass" type="password" placeholder="主口令" style="width:100%;box-sizing:border-box;background:#0b1424;border:1px solid #2b4a66;color:#dff1ff;border-radius:3px;padding:7px 9px;font:12px ui-monospace,Menlo,monospace">'
1364
+ +'<div style="margin-top:6px"><button id="vbGo" style="border-color:#7fe0c0;color:#8ff0d0">解锁</button><span id="vbMsg" style="font-size:10px;opacity:.75;margin-left:8px"></span></div>';
1365
+ In.innerHTML=h;
1366
+ document.getElementById('vbGo').onclick=function(){
1367
+ var p=(document.getElementById('vbPass')||{}).value||'';
1368
+ var key=vbKey(p,v.salt,v.iter||3000);
1369
+ try{vbMem={list:JSON.parse(vbDec(v.blob,key)),key:key};vbCache(key);vbRender();}
1370
+ catch(e){vbMsg('口令不对,再试试');}
1371
+ };
1372
+ return;
1373
+ }
1374
+ var L=(vbMem&&vbMem.list)||[],ed=vbEdit;
1375
+ h='<div style="font-size:11px;opacity:.85;margin-bottom:6px">🔐 本地密码本 <span style="opacity:.55;font-size:10px">只存本机 · '+L.length+' 条'+(v.lock?' · 已上锁':'')+'</span>';
1376
+ if(v.lock)h+='<button id="vbLock" style="float:right;font-size:10px;border-color:#ffd23f;color:#ffd66b">🔒 锁定</button>';
1377
+ h+='</div>';
1378
+ for(var i=0;i<L.length;i++){var e=L[i];
1379
+ h+='<div style="border:1px solid #1d3550;border-radius:3px;padding:6px 8px;margin-bottom:5px;background:#0a1220">'
1380
+ +'<div style="font-size:11px;color:#dff1ff"><b>'+vbEsc(e.n||'(未命名)')+'</b>'+(e.a?' · <span style="opacity:.55">'+vbEsc(e.a)+'</span>':'')+'</div>'
1381
+ +'<div style="font:11px ui-monospace,Menlo,monospace;color:#8ff0d0;margin-top:2px;word-break:break-all">'+vbEsc(e.p||'')+'</div>'
1382
+ +(e.r?'<div style="font-size:10px;opacity:.6;margin-top:2px">'+vbEsc(e.r)+'</div>':'')
1383
+ +'<div style="margin-top:4px"><button data-vbc="'+i+'" style="font-size:10px">复制</button> <button data-vbe="'+i+'" style="font-size:10px">编辑</button> <button data-vbr="'+i+'" style="font-size:10px;border-color:#ff5f57;color:#ff8a84">删除</button></div></div>';
1384
+ }
1385
+ if(!L.length)h+='<div style="font-size:10px;opacity:.6;margin-bottom:6px">还空着——从下面加第一条吧</div>';
1386
+ var E=(ed>=0&&L[ed])?L[ed]:{n:'',a:'',p:'',r:''};
1387
+ var IS='width:100%;box-sizing:border-box;background:#0b1424;border:1px solid #2b4a66;color:#dff1ff;border-radius:3px;padding:6px 8px;font:12px ui-monospace,Menlo,monospace;margin-top:4px';
1388
+ h+='<div style="border-top:1px dashed #2b4a66;padding-top:7px"><div style="font-size:10px;opacity:.65">'+(ed>=0?'编辑条目':'新增条目')+'</div>'
1389
+ +'<input id="vbN" value="'+vbEsc(E.n)+'" placeholder="名称(必填)· 例:DeepSeek API Key" style="'+IS+'">'
1390
+ +'<input id="vbA" value="'+vbEsc(E.a)+'" placeholder="账号(可空)" style="'+IS+'">'
1391
+ +'<input id="vbP" value="'+vbEsc(E.p)+'" placeholder="密码 / 密钥(必填)" style="'+IS+'">'
1392
+ +'<input id="vbR" value="'+vbEsc(E.r)+'" placeholder="备注(可空)" style="'+IS+'">'
1393
+ +'<div style="margin-top:6px"><button id="vbSv" style="border-color:#7fe0c0;color:#8ff0d0">'+(ed>=0?'保存修改':'+ 添加')+'</button>'+(ed>=0?' <button id="vbCn">取消编辑</button>':'')
1394
+ +'<span id="vbMsg" style="font-size:10px;opacity:.75;margin-left:8px"></span></div></div>';
1395
+ In.innerHTML=h;
1396
+ var lk=document.getElementById('vbLock');if(lk)lk.onclick=function(){vbCache(null);vbMem=null;vbRender();};
1397
+ for(var k2=0;k2<L.length;k2++){(function(idx){
1398
+ var c=In.querySelector('[data-vbc="'+idx+'"]');
1399
+ if(c)c.onclick=function(){vbMsg(vbCopy(L[idx].p||'')?'已复制 ✓':'复制失败,长按手动复制')};
1400
+ var eb2=In.querySelector('[data-vbe="'+idx+'"]');
1401
+ if(eb2)eb2.onclick=function(){vbEdit=idx;vbRender();};
1402
+ var rm=In.querySelector('[data-vbr="'+idx+'"]');
1403
+ if(rm)rm.onclick=function(){if(rm.getAttribute('data-arm')!=='1'){rm.setAttribute('data-arm','1');rm.textContent='再点=删除';return}L.splice(idx,1);vbPersist();vbEdit=-1;vbRender();};
1404
+ })(k2);}
1405
+ var sv=document.getElementById('vbSv');
1406
+ if(sv)sv.onclick=function(){
1407
+ var n=((document.getElementById('vbN')||{}).value||'').trim();
1408
+ var a=((document.getElementById('vbA')||{}).value||'').trim();
1409
+ var p=((document.getElementById('vbP')||{}).value||'').trim();
1410
+ var r=((document.getElementById('vbR')||{}).value||'').trim();
1411
+ if(!n){vbMsg('写个名称吧');return}
1412
+ if(!p){vbMsg('密码/密钥别忘了填');return}
1413
+ var it={n:n,a:a,p:p,r:r,ts:Date.now()};
1414
+ if(vbEdit>=0&&L[vbEdit])L[vbEdit]=it;else L.push(it);
1415
+ vbPersist();vbEdit=-1;vbMsg('已保存 ✓');
1416
+ setTimeout(function(){vbRender()},350);
1417
+ };
1418
+ var cn=document.getElementById('vbCn');
1419
+ if(cn)cn.onclick=function(){vbEdit=-1;vbRender();};
1420
+ }
1252
1421
  window.__shaT=sha;window.__pushT=push;window.__pullT=pull;window.__canonT=canon;window.__acct={admin:function(){return !!(acct&&acct.k===ADMIN_K)}};
1253
1422
  })();
1254
1423
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prts-moonlit",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "ARKNIGHTS x P3R Moonlit PRTS terminal (personal toy site)",
5
5
  "license": "MIT"
6
6
  }