prts-moonlit 1.2.1 → 1.2.2

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 +143 -3
  2. package/index.html +167 -12
  3. package/ledger.html +173 -4
  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{
@@ -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;
@@ -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.2';
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.2 正式上线!<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.2 正式上线!</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,9 @@ 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.2</div>'+
2655
+ '<div style="font-size:.82em;line-height:1.8;margin-top:6px">✦ 本地密码本:@ 面板 🔐 · 只存本机、主口令加密、永不上云<br>✦ 收件箱:意见与战绩直达站内,主人专属(📬)<br>✦ AI 推理调节:对话页 ⚙ 新增 推理强度 / 温度 / 上限<br>✦ 账本余额校准:「💳 实际余额」自己填,主页余额同步跟动</div></div>'+
2656
+ '<div class="box" style="margin-top:10px"><div class="tag">里程碑 · v1.2.1</div>'+
2647
2657
  '<div style="font-size:.82em;line-height:1.8;margin-top:6px">✦ 意见箱上岗:@ 面板 📮 写信直达云端信箱,鲸鲸定期收信<br>✦ 一键报备:寻访战绩直传云端;和 AI 聊一句自动打卡<br>✦ API Key 加密保存:密文落盘、聊天照常<br>✦ 版本哨兵:新版上线自动提醒「点此刷新更新」<br>✦ 公开招募下线(九宫格重排 01-09)· 免责声明上站</div></div>'+
2648
2658
  '<div class="box" style="margin-top:10px"><div class="tag">里程碑 · v1.2.0</div>'+
2649
2659
  '<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 +2801,12 @@ function handleAct(t){
2791
2801
  else if(a==='autofill'){S.squads[S.curSquad]=['makoto','aigis','mitsuru','akihiko','yukari','fuuka'];
2792
2802
  sfx.chime();toast('已按战力自动编成');V.squads()}
2793
2803
  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()}
2804
+ 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}
2805
+ var _r=$('#aiR')?$('#aiR').value:'',_tp=$('#aiTp')?$('#aiTp').value.trim():'',_mt=$('#aiMt')?$('#aiMt').value.trim():'';
2806
+ if(_tp!==''&&(isNaN(_tp)||Number(_tp)<0||Number(_tp)>2)){sfx.deny();toast('温度要在 0~2 之间');return}
2807
+ if(_mt!==''&&!/^\d+$/.test(_mt)){sfx.deny();toast('tokens 要填正整数');return}
2808
+ var _nc={u:_u,k:aiEnc(_k),m:_m};if(_r)_nc.r=_r;if(_tp!=='')_nc.tp=_tp;if(_mt!=='')_nc.mt=_mt;
2809
+ aiSave(AICFG_K,_nc);aiFormOpen=0;sfx.ok();toast('API 配置已保存');V.chat()}
2795
2810
  else if(a==='aisend'){var _v=($('#aiInp').value||'').trim();if(!_v&&!aiDraftImg){sfx.deny();return}$('#aiInp').value='';aiSend(_v)}
2796
2811
  else if(a==='aiclear'){var _p=aiActive(),_c=aiChats();_c[_p.id]=[];aiSave(AICHAT_K,_c);sfx.tap();toast('对话已清空');V.chat()}
2797
2812
  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 +2991,7 @@ boot();
2976
2991
  (function(){
2977
2992
  var B=document.getElementById('petB'),bub=document.getElementById('petBub'),txt=document.getElementById('petTxt');
2978
2993
  if(!B)return;
2979
- var L=['鲸鲸~','主人~','捏到了!再捏一下嘛','今日宜抽卡','小鱼干 +1!','BUILD v1.2.1 已就位','账单同步了吗?','过场动画,好看吧?','晚安前记得说晚安哦','鲸鲸在看家…','要摸摸头吗?','别熬夜啦,主人'];
2994
+ var L=['鲸鲸~','主人~','捏到了!再捏一下嘛','今日宜抽卡','小鱼干 +1!','BUILD v1.2.2 已就位','账单同步了吗?','过场动画,好看吧?','晚安前记得说晚安哦','鲸鲸在看家…','要摸摸头吗?','别熬夜啦,主人'];
2980
2995
  var last=-1,ht=0;
2981
2996
  function say(){
2982
2997
  var i;do{i=Math.floor(Math.random()*L.length)}while(i===last);
@@ -3011,13 +3026,13 @@ boot();
3011
3026
  setInterval(function(){if(document.visibilityState==='visible')say()},36000);
3012
3027
  })();
3013
3028
  </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>
3029
+ <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.2</div>
3015
3030
 
3016
3031
  <!-- ===== SYSTEM LOGIN 云存档 ===== -->
3017
3032
  <script>
3018
3033
  /* ===== SYSTEM LOGIN · 云存档同步引擎 v1 ===== */
3019
3034
  (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'];
3035
+ 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
3036
  var BASE='https://textdb.dev/api/data/prts-acct-';var ADMIN_K='e74aeb1a34c44f2fcefd1f19bb34c3891a4963f36242e5a5fd3869085892a711';
3022
3037
  var LS='whale.acct',LT='whale.sync.t';
3023
3038
  var acct=null;try{acct=JSON.parse(localStorage.getItem(LS)||'null')}catch(e){}
@@ -3051,6 +3066,33 @@ boot();
3051
3066
  }
3052
3067
  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
3068
  function url(){return BASE+acct.k;}
3069
+ function mbFetch(cb){
3070
+ try{
3071
+ var x=new XMLHttpRequest();x.open('GET','https://textdb.dev/api/data/prts-myb-mailbox?ts='+Date.now(),true);x.timeout=20000;
3072
+ x.onload=function(){try{var cur=JSON.parse((''+x.responseText).replace(/=$/,''))||[];if(!(cur instanceof Array))cur=[];cb(cur)}catch(e){cb(null)}};
3073
+ x.onerror=function(){cb(null)};x.send();
3074
+ }catch(e){cb(null)}
3075
+ }
3076
+ function mbEsc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
3077
+ function mbRender(){
3078
+ var In=document.getElementById('mbIn');if(!In)return;
3079
+ if(!(window.__acct&&window.__acct.admin&&window.__acct.admin())){
3080
+ In.innerHTML='<div style="font-size:10px;opacity:.65">📬 收件箱 · 仅主人可见(先用你的账号登录再来看)</div>';return;
3081
+ }
3082
+ In.innerHTML='<div style="font-size:10px;opacity:.7">⏳ 收信中…</div>';
3083
+ mbFetch(function(list){
3084
+ var In2=document.getElementById('mbIn');if(!In2)return;
3085
+ if(list===null){In2.innerHTML='<div style="font-size:10px;opacity:.7">⚠ 收信失败 <button id="mbRe" style="font-size:10px">重试</button></div>';bindRe();return;}
3086
+ 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>';
3087
+ for(var i=0;i<list.length;i++){var m=list[i]||{};var d=new Date(m.ts||0);
3088
+ var ts=('0'+(d.getMonth()+1)).slice(-2)+'-'+('0'+d.getDate()).slice(-2)+' '+('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2);
3089
+ 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>';
3090
+ }
3091
+ if(!list.length)h+='<div style="font-size:10px;opacity:.6">还没有来信~</div>';
3092
+ In2.innerHTML=h;bindRe();
3093
+ function bindRe(){var r3=document.getElementById('mbRe');if(r3)r3.onclick=function(){mbRender();};}
3094
+ });
3095
+ }
3054
3096
  function mbPost(kind,txt,cb){
3055
3097
  var u='https://textdb.dev/api/data/prts-myb-mailbox';
3056
3098
  try{
@@ -3142,11 +3184,15 @@ boot();
3142
3184
  }
3143
3185
  h+='<button id="acExp">💾 本地存档码</button><button id="acImp">📥 存档码导入</button><button id="acWipe" style="border-color:#ff5f57;color:#ff8a84">🧹 清空本机存档</button>'+
3144
3186
  '<button id="acFb" style="border-color:#ffd23f;color:#ffd66b">📮 意见箱</button>'+
3187
+ '<button id="vbBtn" style="border-color:#7fe0c0;color:#8ff0d0">🔐 本地密码本</button>'+
3188
+ (window.__acct&&window.__acct.admin&&window.__acct.admin()?'<button id="mbBtn" style="border-color:#9fd0ff;color:#c8e4ff">📬 收件箱</button>':'')+
3145
3189
  '<div class="st" id="acSt"></div><textarea id="acTa" spellcheck="false"></textarea>'+
3146
3190
  '<div id="fbBox" style="display:none;margin-top:10px;border-top:1px dashed #2b4a66;padding-top:9px">'+
3147
3191
  '<div style="font-size:11px;opacity:.78;margin-bottom:5px">📮 意见箱 · 内容直达云端信箱(可匿名),鲸鲸会定期收信</div>'+
3148
3192
  '<textarea id="fbTxt" spellcheck="false" style="display:block;height:64px" placeholder="想说的话、想要的更新、发现的 bug……"></textarea>'+
3149
3193
  '<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>'+
3194
+ '<div id="vbBox" style="display:none;margin-top:10px;border-top:1px dashed #2b4a66;padding-top:9px"><div id="vbIn"></div></div>'+
3195
+ '<div id="mbBox" style="display:none;margin-top:10px;border-top:1px dashed #2b4a66;padding-top:9px"><div id="mbIn"></div></div>'+
3150
3196
  '<div style="font-size:10px;opacity:.5;margin-top:9px;line-height:1.5">密钥由「用户名+密码」派生;同一账号在任意设备登录即接回数据。忘记密码=新仓库(旧数据找不回)</div>'+
3151
3197
  '<button id="acX" style="border-color:#2b4a66;color:#8ea6bf;margin-top:10px">关闭 / CLOSE</button></div>';
3152
3198
  wrap.innerHTML=h;
@@ -3201,7 +3247,7 @@ boot();
3201
3247
  var wp=g('#acWipe');
3202
3248
  if(wp)wp.onclick=function(){
3203
3249
  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);}
3250
+ 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
3251
  for(var j=0;j<ks.length;j++)localStorage.removeItem(ks[j]);}catch(e){}
3206
3252
  location.reload();return;}
3207
3253
  wp.setAttribute('data-c','1');wp.textContent='⚠ 再点一次=清空并刷新(云端不受影响)';};
@@ -3213,6 +3259,8 @@ boot();
3213
3259
  if(st2)st2.textContent='寄送中…';
3214
3260
  mbPost('意见',t2,function(ok){if(st2)st2.textContent=ok?'✅ 已寄出,谢谢!鲸鲸会看到的':'⚠ 寄送失败,稍后再试';if(ok&&t)t.value='';});
3215
3261
  };
3262
+ 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();};
3263
+ 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
3264
  var x=g('#acX');if(x)x.onclick=function(){wrap.classList.remove('on')};
3217
3265
  wrap.onclick=function(e){if(e.target===wrap)wrap.classList.remove('on')};
3218
3266
  }
@@ -3235,7 +3283,7 @@ boot();
3235
3283
  btn.addEventListener('click',open);
3236
3284
  setInterval(function(){if(acct&&acct.k){var c=canon();if(c!==lastSnap){lastSnap=c;clearTimeout(pq);pq=setTimeout(push,900);}}},2000);
3237
3285
  if(acct&&acct.k){setTimeout(function(){pull(false);},250);}
3238
- var APP_VER='1.2.1';
3286
+ var APP_VER='1.2.2';
3239
3287
  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
3288
  function chkVer(){
3241
3289
  try{
@@ -3257,6 +3305,113 @@ function chkVer(){
3257
3305
  }
3258
3306
  setTimeout(chkVer,2600);
3259
3307
  setInterval(chkVer,600000);
3308
+ /* ===== 🔐 本地密码本(whale.vault.v1 · 只存本机 · 不入同步/存档码/清空存档)===== */
3309
+ var VBK='whale.vault.v1',vbMem=null,vbEdit=-1;
3310
+ function vbRead(){try{return JSON.parse(localStorage.getItem(VBK)||'null')}catch(e){return null}}
3311
+ function vbSave(o){try{localStorage.setItem(VBK,JSON.stringify(o));return 1}catch(e){return 0}}
3312
+ function vbCache(s){try{if(s===null)sessionStorage.removeItem('whale.vault.un');else sessionStorage.setItem('whale.vault.un',s)}catch(e){}}
3313
+ function vbCached(){try{return sessionStorage.getItem('whale.vault.un')||''}catch(e){return ''}}
3314
+ function vbSalt(){var s='',h='0123456789abcdef';for(var i=0;i<16;i++)s+=h.charAt(Math.floor(Math.random()*16));return s;}
3315
+ function vbKey(pass,salt,iter){var x=salt+'|'+pass;for(var i=0;i<iter;i++)x=sha(x);return x;}
3316
+ function vbXor(s,key){var o='';for(var b=0;b<s.length;b+=32){var blk=sha(key+'|'+(b/32|0));
3317
+ 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;}
3318
+ function vbEnc(t,key){return btoa(vbXor(unescape(encodeURIComponent(t)),key));}
3319
+ function vbDec(c,key){return decodeURIComponent(escape(vbXor(atob(c),key)));}
3320
+ function vbEsc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
3321
+ function vbMsg(t){var m=document.getElementById('vbMsg');if(m)m.textContent=t;}
3322
+ function vbCopy(t){var ok=0;try{navigator.clipboard.writeText(t);ok=1}catch(e){}
3323
+ 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){}}
3324
+ return ok;}
3325
+ function vbLoad(){
3326
+ var v=vbRead();if(!v){return}
3327
+ if(!v.lock){vbMem={list:(v.list||[]),key:''};return}
3328
+ var ck=vbCached();if(!ck){vbMem=null;return}
3329
+ try{vbMem={list:JSON.parse(vbDec(v.blob,ck)),key:ck}}catch(e){vbCache(null);vbMem=null}
3330
+ }
3331
+ function vbPersist(){
3332
+ var v=vbRead();if(!v||!vbMem)return;
3333
+ if(v.lock){v.blob=vbEnc(JSON.stringify(vbMem.list),vbMem.key)}
3334
+ else{v.list=vbMem.list}
3335
+ vbSave(v);
3336
+ }
3337
+ function vbRender(){
3338
+ var In=document.getElementById('vbIn');if(!In)return;
3339
+ var v=vbRead(),h='';
3340
+ if(!v){
3341
+ h='<div style="font-size:11px;opacity:.85;margin-bottom:5px">🔐 本地密码本 · <span style="opacity:.55;font-size:10px">只存本机 · 永不云同步 · 不随存档导出</span></div>'
3342
+ +'<div style="font-size:10px;opacity:.6;margin-bottom:6px">第一次使用:设一个主口令(留空 = 不加锁)</div>'
3343
+ +'<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">'
3344
+ +'<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>';
3345
+ In.innerHTML=h;
3346
+ document.getElementById('vbGo').onclick=function(){
3347
+ var p=(document.getElementById('vbPass')||{}).value||'';
3348
+ if(!p){vbSave({v:1,lock:0,list:[]});vbMem={list:[],key:''};vbRender();return}
3349
+ var salt=vbSalt(),iter=3000,key=vbKey(p,salt,iter);
3350
+ vbSave({v:1,lock:1,salt:salt,iter:iter,blob:vbEnc('[]',key)});
3351
+ vbCache(key);vbMem={list:[],key:key};vbRender();
3352
+ };
3353
+ return;
3354
+ }
3355
+ if(v.lock&&!vbMem){
3356
+ h='<div style="font-size:11px;opacity:.85;margin-bottom:5px">🔐 本地密码本 · <b style="color:#ffd23f">已锁定</b></div>'
3357
+ +'<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">'
3358
+ +'<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>';
3359
+ In.innerHTML=h;
3360
+ document.getElementById('vbGo').onclick=function(){
3361
+ var p=(document.getElementById('vbPass')||{}).value||'';
3362
+ var key=vbKey(p,v.salt,v.iter||3000);
3363
+ try{vbMem={list:JSON.parse(vbDec(v.blob,key)),key:key};vbCache(key);vbRender();}
3364
+ catch(e){vbMsg('口令不对,再试试');}
3365
+ };
3366
+ return;
3367
+ }
3368
+ var L=(vbMem&&vbMem.list)||[],ed=vbEdit;
3369
+ h='<div style="font-size:11px;opacity:.85;margin-bottom:6px">🔐 本地密码本 <span style="opacity:.55;font-size:10px">只存本机 · '+L.length+' 条'+(v.lock?' · 已上锁':'')+'</span>';
3370
+ if(v.lock)h+='<button id="vbLock" style="float:right;font-size:10px;border-color:#ffd23f;color:#ffd66b">🔒 锁定</button>';
3371
+ h+='</div>';
3372
+ for(var i=0;i<L.length;i++){var e=L[i];
3373
+ h+='<div style="border:1px solid #1d3550;border-radius:3px;padding:6px 8px;margin-bottom:5px;background:#0a1220">'
3374
+ +'<div style="font-size:11px;color:#dff1ff"><b>'+vbEsc(e.n||'(未命名)')+'</b>'+(e.a?' · <span style="opacity:.55">'+vbEsc(e.a)+'</span>':'')+'</div>'
3375
+ +'<div style="font:11px ui-monospace,Menlo,monospace;color:#8ff0d0;margin-top:2px;word-break:break-all">'+vbEsc(e.p||'')+'</div>'
3376
+ +(e.r?'<div style="font-size:10px;opacity:.6;margin-top:2px">'+vbEsc(e.r)+'</div>':'')
3377
+ +'<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>';
3378
+ }
3379
+ if(!L.length)h+='<div style="font-size:10px;opacity:.6;margin-bottom:6px">还空着——从下面加第一条吧</div>';
3380
+ var E=(ed>=0&&L[ed])?L[ed]:{n:'',a:'',p:'',r:''};
3381
+ 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';
3382
+ h+='<div style="border-top:1px dashed #2b4a66;padding-top:7px"><div style="font-size:10px;opacity:.65">'+(ed>=0?'编辑条目':'新增条目')+'</div>'
3383
+ +'<input id="vbN" value="'+vbEsc(E.n)+'" placeholder="名称(必填)· 例:DeepSeek API Key" style="'+IS+'">'
3384
+ +'<input id="vbA" value="'+vbEsc(E.a)+'" placeholder="账号(可空)" style="'+IS+'">'
3385
+ +'<input id="vbP" value="'+vbEsc(E.p)+'" placeholder="密码 / 密钥(必填)" style="'+IS+'">'
3386
+ +'<input id="vbR" value="'+vbEsc(E.r)+'" placeholder="备注(可空)" style="'+IS+'">'
3387
+ +'<div style="margin-top:6px"><button id="vbSv" style="border-color:#7fe0c0;color:#8ff0d0">'+(ed>=0?'保存修改':'+ 添加')+'</button>'+(ed>=0?' <button id="vbCn">取消编辑</button>':'')
3388
+ +'<span id="vbMsg" style="font-size:10px;opacity:.75;margin-left:8px"></span></div></div>';
3389
+ In.innerHTML=h;
3390
+ var lk=document.getElementById('vbLock');if(lk)lk.onclick=function(){vbCache(null);vbMem=null;vbRender();};
3391
+ for(var k2=0;k2<L.length;k2++){(function(idx){
3392
+ var c=In.querySelector('[data-vbc="'+idx+'"]');
3393
+ if(c)c.onclick=function(){vbMsg(vbCopy(L[idx].p||'')?'已复制 ✓':'复制失败,长按手动复制')};
3394
+ var eb2=In.querySelector('[data-vbe="'+idx+'"]');
3395
+ if(eb2)eb2.onclick=function(){vbEdit=idx;vbRender();};
3396
+ var rm=In.querySelector('[data-vbr="'+idx+'"]');
3397
+ 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();};
3398
+ })(k2);}
3399
+ var sv=document.getElementById('vbSv');
3400
+ if(sv)sv.onclick=function(){
3401
+ var n=((document.getElementById('vbN')||{}).value||'').trim();
3402
+ var a=((document.getElementById('vbA')||{}).value||'').trim();
3403
+ var p=((document.getElementById('vbP')||{}).value||'').trim();
3404
+ var r=((document.getElementById('vbR')||{}).value||'').trim();
3405
+ if(!n){vbMsg('写个名称吧');return}
3406
+ if(!p){vbMsg('密码/密钥别忘了填');return}
3407
+ var it={n:n,a:a,p:p,r:r,ts:Date.now()};
3408
+ if(vbEdit>=0&&L[vbEdit])L[vbEdit]=it;else L.push(it);
3409
+ vbPersist();vbEdit=-1;vbMsg('已保存 ✓');
3410
+ setTimeout(function(){vbRender()},350);
3411
+ };
3412
+ var cn=document.getElementById('vbCn');
3413
+ if(cn)cn.onclick=function(){vbEdit=-1;vbRender();};
3414
+ }
3260
3415
  window.__shaT=sha;window.__pushT=push;window.__pullT=pull;window.__canonT=canon;window.__acct={admin:function(){return !!(acct&&acct.k===ADMIN_K)}};
3261
3416
  })();
3262
3417
  </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{
@@ -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;
@@ -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.2';
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.2",
4
4
  "description": "ARKNIGHTS x P3R Moonlit PRTS terminal (personal toy site)",
5
5
  "license": "MIT"
6
6
  }