Initial commit: 自然记忆站:12 页纯标准库 Python 静态站点生成器,含真实评测台账(108 条)与全站客户端检索

This commit is contained in:
WpyQwq
2026-09-19 11:21:45 +08:00
commit 1da4b14c19
44 changed files with 9624 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Natural Memory v2 — 站点服务端
纯标准库实现,无第三方依赖(目标环境 Python 3.8+)。
默认监听 127.0.0.1:7443(仅本机可达,供 Cloudflare Tunnel 等反向代理使用)。
如需对外直接监听,显式指定 --host 0.0.0.0。
用法:
python server.py
python server.py --host 0.0.0.0 --port 7443
set NM_PORT=8443 && python server.py
设计约定(安全默认):
只放行 index.html 与 assets/ 下白名单扩展名的文件,其余全部 404。
server.py、启动脚本、说明文档等一律不通过 HTTP 暴露;目录列表关闭。
任何包含 .. / . / 反斜杠 / NUL 的路径直接拒绝。
"""
from __future__ import annotations
import argparse
import json
import mimetypes
import os
import sys
import time
from datetime import datetime, timezone
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import unquote, urlparse
ROOT = os.path.dirname(os.path.abspath(__file__))
SITE_VERSION = "v2"
STARTED = time.time()
# Python 3.8 的 mimetypes 不认 woff2,需要显式登记
mimetypes.add_type("font/woff2", ".woff2")
mimetypes.add_type("font/woff", ".woff")
mimetypes.add_type("application/javascript", ".js")
mimetypes.add_type("image/svg+xml", ".svg")
ALLOWED_EXT = {
".html", ".css", ".js", ".mjs", ".json",
".woff2", ".woff", ".ttf",
".svg", ".png", ".jpg", ".jpeg", ".webp", ".avif", ".ico",
}
ALLOWED_ROOT_FILES = {"index.html", "favicon.ico", "robots.txt"}
# 生成出来的分页(build.py 的 PAGES 列表)。支持 /division 与 /division.html 两种写法。
PAGE_SLUGS = {
"index", "division", "mechanism", "results", "nm21", "examples",
"ledger", "cost", "capacity", "limits", "roadmap", "reproduce",
}
LONG_CACHE = (".woff2", ".woff", ".ttf", ".png", ".jpg", ".jpeg", ".webp", ".avif", ".svg", ".ico")
NO_CACHE = (".html", ".css", ".js", ".json")
def human_uptime(seconds: float) -> str:
seconds = int(seconds)
d, rem = divmod(seconds, 86400)
h, rem = divmod(rem, 3600)
m, s = divmod(rem, 60)
if d:
return "%dd %dh" % (d, h)
if h:
return "%dh %dm" % (h, m)
if m:
return "%dm %ds" % (m, s)
return "%ds" % s
class Handler(SimpleHTTPRequestHandler):
server_version = "NaturalMemorySite/" + SITE_VERSION
protocol_version = "HTTP/1.1"
# ---------------- 路径安全 ----------------
@staticmethod
def _safe_rel(path: str):
"""把请求路径解析为站点内的相对路径;不合规返回 None。"""
raw = unquote(path or "")
if "\x00" in raw or "\\" in raw:
return None
segs = raw.split("/")
if any(s in (".", "..") for s in segs):
return None
segs = [s for s in segs if s]
if not segs:
return "index.html"
if len(segs) == 1:
name = segs[0]
if name in ALLOWED_ROOT_FILES:
return name
# 干净 URL:/division → division.html
if name in PAGE_SLUGS:
return "index.html" if name == "index" else "%s.html" % name
if name.endswith(".html") and name[:-5] in PAGE_SLUGS:
return name
return None
if segs[0] == "assets" and os.path.splitext(segs[-1])[1].lower() in ALLOWED_EXT:
return "/".join(segs)
return None
# ---------------- 运行时读数 ----------------
def _status_payload(self) -> bytes:
host, port = self.server.server_address[0], self.server.server_address[1]
payload = {
"site": "Natural Memory " + SITE_VERSION,
"note": "以下为本站 Web 服务进程的读数,不是模型运行时的指标。",
"host": getattr(self.server, "display_host", host),
"bind": "%s:%d" % (host, port),
"python": "%d.%d.%d" % sys.version_info[:3],
"uptime": human_uptime(time.time() - STARTED),
"uptime_seconds": round(time.time() - STARTED, 1),
"requests": getattr(self.server, "request_count", 0),
"started": datetime.fromtimestamp(STARTED).astimezone().isoformat(timespec="seconds"),
"now": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"),
}
return json.dumps(payload, ensure_ascii=False).encode("utf-8")
def _send_json(self, body: bytes, status: int = 200) -> None:
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
if self.command != "HEAD":
self.wfile.write(body)
# ---------------- 路由 ----------------
def _dispatch(self, with_body: bool) -> None:
self.server.request_count = getattr(self.server, "request_count", 0) + 1
path = urlparse(self.path).path
if path.rstrip("/") == "/api/status":
self._send_json(self._status_payload())
return
if path == "/healthz":
self._send_json(b'{"ok":true}')
return
rel = self._safe_rel(path)
if rel is None:
# 不区分「不存在」与「不允许」,避免泄露文件是否存在
self.send_error(404, "Not Found")
return
self.path = "/" + rel
if with_body:
SimpleHTTPRequestHandler.do_GET(self)
else:
SimpleHTTPRequestHandler.do_HEAD(self)
def do_GET(self): # noqa: N802
self._dispatch(True)
def do_HEAD(self): # noqa: N802
self._dispatch(False)
# ---------------- 缓存与安全响应头 ----------------
def end_headers(self):
path = urlparse(self.path).path.lower()
if path.endswith(".html"):
# 页面本体永不缓存:模板一变立刻生效
self.send_header("Cache-Control", "no-store")
elif path.endswith(LONG_CACHE):
# 静态资源走构建指纹(?v=hash),可长期不可变缓存
self.send_header("Cache-Control", "public, max-age=604800, immutable")
elif path.endswith(NO_CACHE):
self.send_header("Cache-Control", "no-cache")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("Referrer-Policy", "same-origin")
self.send_header("X-Frame-Options", "SAMEORIGIN")
super().end_headers()
# ---------------- 关闭目录列表 ----------------
def list_directory(self, path):
self.send_error(404, "Not Found")
return None
def log_message(self, fmt, *args):
sys.stderr.write("[%s] %s %s\n" % (
datetime.now().strftime("%H:%M:%S"), self.address_string(), fmt % args,
))
class Server(ThreadingHTTPServer):
"""ThreadingHTTPServer 已含 ThreadingMixIn,这里只补充运行时读数字段。"""
daemon_threads = True
allow_reuse_address = True
request_count = 0
display_host = "127.0.0.1"
def main() -> int:
ap = argparse.ArgumentParser(description="Natural Memory v2 site server")
ap.add_argument("--host", default=os.environ.get("NM_HOST", "127.0.0.1"),
help="监听地址(默认 127.0.0.1,仅本机可达)")
ap.add_argument("--port", type=int, default=int(os.environ.get("NM_PORT", "7443")),
help="监听端口(默认 7443)")
args = ap.parse_args()
if not os.path.isfile(os.path.join(ROOT, "index.html")):
sys.stderr.write("index.html 不在 %s,请从站点根目录启动。\n" % ROOT)
return 2
httpd = Server((args.host, args.port), lambda *a, **kw: Handler(*a, directory=ROOT, **kw))
httpd.display_host = args.host
print("Natural Memory %s — site server" % SITE_VERSION)
print(" 根目录 : %s" % ROOT)
print(" 监听 : http://%s:%d/" % (args.host, args.port))
print(" 读数 : http://%s:%d/api/status" % (args.host, args.port))
if args.host in ("127.0.0.1", "localhost"):
print(" 提示 : 当前仅本机可达;需要对外请用反向代理,或显式 --host 0.0.0.0")
print(" Ctrl+C 退出")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n已停止。")
finally:
httpd.server_close()
return 0
if __name__ == "__main__":
raise SystemExit(main())