记忆台
← 返回 app.py 16.3K · 1 小时前
\"\"\"\n\n\ndef login_page(err=\"\"):\n body = f\"\"\"

\u72b6\u6001\u56de\u9000

\n
QQ / \u5fae\u4fe1 \u5206\u680f \u00b7 \u56de\u9000\u524d\u81ea\u52a8\u6574\u5e93\u5907\u4efd
\n
\n\n
\n{'
' + err + '
' if err else ''}\n
\u53ea\u5728\u8fd9\u53f0\u673a\u5668\u4e0a\u751f\u6548\uff0c\u53e3\u4ee4\u5728 ~/.hermes/private/rollback_pass
\"\"\"\n return PAGE.replace(\"__BODY__\", body).replace(\"__PLAT__\", \"'qqbot'\").replace(\"__SID__\", \"null\")\n\n\ndef main_page(platform, sid):\n label = dict(PLATFORMS).get(platform, platform)\n tabs = \"\"\n for p, name in PLATFORMS:\n cls = \"tab \" + (\"qq\" if p == \"qqbot\" else \"wx\") + (\" on\" if p == platform else \"\")\n tabs += f'
{name}
'\n\n ss = sessions(platform)\n cards = \"\"\n for s in ss:\n live = '\u5728\u7528' if s[\"live\"] else '\u5386\u53f2'\n cards += f\"\"\"
\n
{s['n']} \u6761 {live}
\n
{s['id']}
\n
\u5f00\u59cb {s['started']} \u00b7 \u6700\u540e {s['last']}
\n
\n
\"\"\"\n\n msgbox = \"\"\n if sid:\n ms = messages(sid)\n if ms:\n first = ms[0]\n msgbox = f'
\u6d88\u606f\uff08\u6700\u8fd1 {len(ms)} \u6761\uff09'\\\n f'
'\n for m in ms:\n msgbox += (f'
{m[\"role\"]} \u00b7 {m[\"time\"]} \u00b7 #{m[\"id\"]} '\n f'
'\n f'{html.escape(m[\"t\"] or \"\")}
')\n msgbox += \"
\"\n else:\n msgbox = '
\u8fd9\u6761\u4f1a\u8bdd\u6ca1\u6709\u6d88\u606f\u3002
'\n\n bks = list_backups()\n bkhtml = '
\u6700\u8fd1\u5907\u4efd
\u70b9\u8fd8\u539f\uff1d\u6574\u5e93\u8986\u76d6\u56de\u53bb\uff0c\u8986\u76d6\u524d\u4f1a\u81ea\u52a8\u518d\u5b58\u4e00\u4efd\u5f53\u524d\u5e93
'\n for b in bks[:12]:\n bkhtml += (f'
'\n f'
{b[\"name\"]}
{b[\"mtime\"]} \u00b7 {b[\"size\"]} MB
'\n f'
')\n bkhtml += \"
\"\n\n body = f\"\"\"

\u72b6\u6001\u56de\u9000 \u00b7 {label}

\n
\u9009\u4e00\u6761\u4f1a\u8bdd \u2192 \u770b\u6d88\u606f \u2192 \u56de\u9000\u5230\u67d0\u4e2a\u70b9\u4e4b\u524d\u3002\u56de\u9000\u524d\u6574\u5e93\u5907\u4efd\u3002
\n
{tabs}
\n
\u56de\u9000\u53ea\u5220\u6d88\u606f\uff0c\u4e0d\u52a8 SOUL / \u6863\u6848\u3002
\n{cards}{msgbox}{bkhtml}\n
\"\"\"\n return PAGE.replace(\"__BODY__\", body).replace(\"__PLAT__\", repr(platform)).replace(\"__SID__\", json.dumps(sid))\n\n\nclass H(BaseHTTPRequestHandler):\n server_version = \"rollback/1\"\n\n def _auth(self):\n c = self.headers.get(\"Cookie\", \"\")\n return f\"{COOKIE}={TOKEN}\" in c\n\n def _send(self, code, body, ctype=\"text/html; charset=utf-8\", cookie=None):\n data = body.encode(\"utf-8\") if isinstance(body, str) else body\n self.send_response(code)\n self.send_header(\"Content-Type\", ctype)\n self.send_header(\"Content-Length\", str(len(data)))\n self.send_header(\"Cache-Control\", \"no-store\")\n if cookie:\n self.send_header(\"Set-Cookie\", cookie)\n self.end_headers()\n self.wfile.write(data)\n\n def _json(self, code, obj):\n self._send(code, json.dumps(obj, ensure_ascii=False), \"application/json; charset=utf-8\")\n\n def do_GET(self):\n u = urlparse(self.path)\n if u.path == \"/health\":\n return self._json(200, {\"ok\": True})\n if u.path == \"/login\":\n return self._send(200, login_page())\n if not self._auth():\n return self._send(200, login_page())\n if u.path == \"/\":\n p = parse_qs(u.query)\n plat = (p.get(\"platform\") or [\"qqbot\"])[0]\n sid = (p.get(\"session\") or [None])[0]\n return self._send(200, main_page(plat, sid))\n if u.path == \"/api/sessions\":\n p = parse_qs(u.query)\n return self._json(200, sessions((p.get(\"platform\") or [\"qqbot\"])[0]))\n if u.path == \"/api/backups\":\n return self._json(200, list_backups())\n return self._send(404, \"not found\", \"text/plain; charset=utf-8\")\n\n def do_POST(self):\n u = urlparse(self.path)\n n = int(self.headers.get(\"Content-Length\") or 0)\n raw = self.rfile.read(n) if n else b\"{}\"\n if u.path == \"/login\":\n form = parse_qs(raw.decode(\"utf-8\", \"ignore\"))\n if (form.get(\"p\") or [\"\"])[0] == PASS:\n return self._send(303, \"\", cookie=f\"{COOKIE}={TOKEN}; Path=/; HttpOnly; Max-Age=2592000; SameSite=Lax\")\n return self._send(200, login_page(\"\u53e3\u4ee4\u4e0d\u5bf9\"))\n if not self._auth():\n return self._json(401, {\"error\": \"\u6ca1\u767b\u5f55\"})\n try:\n body = json.loads(raw or b\"{}\")\n except Exception:\n body = {}\n try:\n if u.path == \"/api/rollback\":\n return self._json(200, do_rollback(body.get(\"session\"), body.get(\"mode\"),\n body.get(\"value\"), body.get(\"tag\")))\n if u.path == \"/api/restore\":\n return self._json(200, restore_backup(body.get(\"name\", \"\")))\n except Exception as e:\n return self._json(400, {\"error\": str(e)})\n return self._json(404, {\"error\": \"no such api\"})\n\n\nif __name__ == \"__main__\":\n port = int(os.environ.get(\"PORT\", \"8899\"))\n print(f\"rollback site on 127.0.0.1:{port} pass={PASS}\")\n ThreadingHTTPServer((\"127.0.0.1\", port), H).serve_forever()\n"; const ta=document.getElementById('src'), pv=document.getElementById('preview'); ta.value=text; let dirty=false; const dot=document.getElementById('dot'); function refresh(){ pv.innerHTML=mdToHtml(ta.value); } function grow(){ ta.style.height='auto'; ta.style.height=(ta.scrollHeight+4)+'px'; } function markDirty(){ dirty=true; dot.classList.add('on'); document.getElementById('stat').textContent='未保存'; } ta.addEventListener('input',()=>{ markDirty(); grow(); // 草稿:切走 App 或刷新时先落在本机,回来能捡 const DKEY='memodesk-draft:'+PATH; ta.addEventListener('input',()=>{ try{ localStorage.setItem(DKEY, JSON.stringify({t:ta.value,at:Date.now()})); }catch(e){} }); (function(){ let d=null; try{ d=JSON.parse(localStorage.getItem(DKEY)||'null'); }catch(e){} if(d && d.t && d.t!==ta.value){ const bar=document.createElement('div'); bar.style.cssText='margin:10px 0 0;padding:10px 12px;border:1px solid var(--line);border-radius:12px;background:var(--panel2);font-size:13.5px;display:flex;gap:10px;align-items:center;flex-wrap:wrap'; bar.innerHTML='有个没存的草稿('+new Date(d.at).toLocaleString('zh-CN')+')'; const b1=document.createElement('button'); b1.className='btn'; b1.textContent='捡回来'; b1.onclick=()=>{ ta.value=d.t; markDirty(); grow(); bar.remove(); }; const b2=document.createElement('button'); b2.className='btn ghost'; b2.textContent='丢掉'; b2.onclick=()=>{ localStorage.removeItem(DKEY); bar.remove(); }; bar.appendChild(b1); bar.appendChild(b2); document.getElementById('editor-area').prepend(bar); } })(); }); ta.addEventListener('keydown',e=>{ if(e.key==='Tab'){ e.preventDefault(); const s=ta.selectionStart, en=ta.selectionEnd; ta.value=ta.value.slice(0,s)+' '+ta.value.slice(en); ta.selectionStart=ta.selectionEnd=s+2; markDirty(); grow(); } }); grow(); document.getElementById('toggle').onclick=()=>{ const showing=pv.style.display!=='none'; if(showing){ pv.style.display='none'; ta.style.display='block'; grow(); document.getElementById('toggle').textContent='预览'; } else { refresh(); pv.style.display='block'; ta.style.display='none'; document.getElementById('toggle').textContent='编辑'; } }; function toast(t){ const el=document.getElementById('toast'); el.textContent=t; el.classList.add('show'); setTimeout(()=>el.classList.remove('show'),1600); } async function save(){ const btn=document.getElementById('save'); btn.disabled=true; btn.textContent='存…'; try{ const r=await fetch('./api/save',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({p:PATH,text:ta.value})}); const j=await r.json(); if(!j.ok) throw new Error(j.error||'保存失败'); dirty=false; dot.classList.remove('on'); try{ localStorage.removeItem('memodesk-draft:'+PATH); }catch(e){} document.getElementById('stat').textContent=j.size+' · 刚刚'; btn.textContent='已存'; toast('存好了喵'); setTimeout(()=>btn.textContent='保存',1200); }catch(e){ toast('保存失败:'+e.message); btn.textContent='保存'; } finally{ btn.disabled=false; } } document.getElementById('save').onclick=save; document.addEventListener('keydown',e=>{ if((e.metaKey||e.ctrlKey)&&e.key==='s'){ e.preventDefault(); save(); } }); window.addEventListener('beforeunload',e=>{ if(dirty){ e.preventDefault(); e.returnValue=''; } }); document.getElementById('history').onclick=async()=>{ const box=document.getElementById('histbox'); const list=JSON.parse([]); if(!list.length){ toast('还没有旧稿'); return; } box.style.display='block'; box.innerHTML='
'+list.map(h=>`
${h.id}${h.when} · ${h.size}
`).join('')+'
'; }; async function restore(id){ if(!confirm('用这份旧稿覆盖当前内容?')) return; const r=await fetch('./api/restore',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({p:PATH,id:id})}); const j=await r.json(); if(j.ok){ ta.value=j.text; dirty=true; toast('已载入旧稿,记得保存'); } else toast('恢复失败'); }