记忆台
← 返回 app.py 9.4K · 5 天前
\"\"\"\n\n\nclass Handler(BaseHTTPRequestHandler):\n server_version = \"MemoChat/1.0\"\n protocol_version = \"HTTP/1.1\"\n\n def log_message(self, fmt, *args):\n pass\n\n def do_GET(self):\n if self.path in (\"/\", \"/index.html\"):\n body = PAGE.encode()\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/html; charset=utf-8\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.send_header(\"Cache-Control\", \"no-store\")\n self.end_headers()\n self.wfile.write(body)\n return\n self.send_error(404)\n\n def do_POST(self):\n if self.path != \"/api/chat\":\n self.send_error(404)\n return\n try:\n n = int(self.headers.get(\"Content-Length\", 0))\n payload = json.loads(self.rfile.read(n) or \"{}\")\n except Exception:\n self.send_error(400)\n return\n msgs = payload.get(\"messages\") or []\n req = urllib.request.Request(\n UPSTREAM,\n data=json.dumps({\"model\": MODEL, \"messages\": msgs, \"stream\": True}).encode(),\n headers={\"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {KEY}\",\n \"User-Agent\": \"OpenAI/Python 1.0\"},\n method=\"POST\",\n )\n try:\n up = urllib.request.urlopen(req, timeout=300)\n except urllib.error.HTTPError as e:\n body = e.read()[:400]\n self.send_response(502)\n self.send_header(\"Content-Type\", \"application/json\")\n self.send_header(\"Content-Length\", str(len(body)))\n self.end_headers()\n self.wfile.write(body)\n return\n except Exception as e:\n msg = json.dumps({\"error\": str(e)}).encode()\n self.send_response(502)\n self.send_header(\"Content-Type\", \"application/json\")\n self.send_header(\"Content-Length\", str(len(msg)))\n self.end_headers()\n self.wfile.write(msg)\n return\n\n self.send_response(200)\n self.send_header(\"Content-Type\", \"text/event-stream; charset=utf-8\")\n self.send_header(\"Cache-Control\", \"no-cache\")\n self.send_header(\"X-Accel-Buffering\", \"no\")\n self.end_headers()\n try:\n while True:\n chunk = up.readline()\n if not chunk:\n break\n self.wfile.write(chunk)\n self.wfile.flush()\n except Exception:\n pass\n finally:\n try:\n up.close()\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n ThreadingHTTPServer((\"127.0.0.1\", 8095), Handler).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('恢复失败'); }