User edition: prune docs, license gate, obfuscate core lib.

Keep deploy/basic docs only; integrate sq.bz121.com license client; encrypt strategy/trade/key_monitor/options/hedge_plan for release.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-17 16:30:58 +08:00
parent 53863559f4
commit 16ef0f31e3
154 changed files with 1998 additions and 27914 deletions
+19
View File
@@ -0,0 +1,19 @@
"""整机许可:设备绑定、激活码兑换、联网校验。"""
from lib.license.license_lib import (
ensure_license_or_raise,
get_device_id,
get_license_status,
is_license_valid,
redeem_code,
validate_license,
)
__all__ = [
"ensure_license_or_raise",
"get_device_id",
"get_license_status",
"is_license_valid",
"redeem_code",
"validate_license",
]
+115
View File
@@ -0,0 +1,115 @@
"""FastAPI 中控许可中间件与 /license 页。"""
from __future__ import annotations
import os
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from lib.license.license_lib import (
get_device_id,
get_license_status,
is_license_valid,
redeem_code,
validate_license,
)
_TEMPLATE_PATH = Path(__file__).resolve().parent / "templates" / "license.html"
def _allowed_path(path: str) -> bool:
if path in (
"/license",
"/api/license/status",
"/api/license/redeem",
"/api/license/validate",
"/health",
):
return True
if path.startswith("/assets/") or path.startswith("/static/"):
return True
if path.startswith("/favicon"):
return True
return False
def install_license_middleware(app: FastAPI) -> None:
@app.get("/health")
async def _license_health():
st = get_license_status(skip_remote=True)
return {"ok": True, "license_valid": bool(st.get("valid")), "service": "manual_trading_hub"}
@app.get("/api/license/status")
async def _license_status_api():
return get_license_status()
@app.post("/api/license/redeem")
async def _license_redeem_api(request: Request):
try:
data = await request.json()
except Exception:
data = {}
code = (data.get("code") if isinstance(data, dict) else "") or ""
return redeem_code(str(code))
@app.post("/api/license/validate")
async def _license_validate_api():
return validate_license(force=True)
@app.api_route("/license", methods=["GET", "POST"])
async def _license_page(request: Request):
msg = ""
err = ""
if request.method == "POST":
form = await request.form()
code = str(form.get("code") or "").strip()
result = redeem_code(code)
if result.get("ok"):
msg = result.get("message") or "激活成功"
else:
err = result.get("message") or "激活失败"
status = get_license_status()
html = _TEMPLATE_PATH.read_text(encoding="utf-8")
# 简单替换,避免 Jinja 依赖差异
filled = (
html.replace("{{ device_id }}", get_device_id())
.replace("{{ api_url }}", str(status.get("api_url") or ""))
.replace("{{ wechat }}", "dekun03")
.replace("{{ message }}", msg)
.replace("{{ error }}", err)
.replace("{{ status_message }}", str(status.get("message") or ""))
.replace("{{ expires_at }}", str(status.get("expires_at") or ""))
.replace("{{ plan }}", str(status.get("plan") or ""))
.replace("{{ valid_text }}", "已授权" if status.get("valid") else "未授权")
)
# Flask 模板用 JinjaFastAPI 路径用占位符版本
if "{%" in filled or "{{" in filled:
from jinja2 import Template
filled = Template(html).render(
device_id=get_device_id(),
status=status,
message=msg,
error=err,
api_url=status.get("api_url") or "",
wechat="dekun03",
)
return HTMLResponse(filled)
@app.middleware("http")
async def _license_http_middleware(request: Request, call_next):
if os.getenv("LICENSE_DISABLED", "").strip().lower() in ("1", "true", "yes", "on"):
return await call_next(request)
path = request.url.path or "/"
if _allowed_path(path):
return await call_next(request)
if is_license_valid():
return await call_next(request)
if path.startswith("/api/"):
return JSONResponse(
{"ok": False, "error": "license_required", "message": "请先激活许可"},
status_code=403,
)
return RedirectResponse(url="/license", status_code=302)
+88
View File
@@ -0,0 +1,88 @@
"""Flask 实例许可门禁与 /license 页。"""
from __future__ import annotations
import os
from pathlib import Path
from flask import Flask, jsonify, redirect, render_template_string, request
from lib.license.license_lib import (
get_device_id,
get_license_status,
is_license_valid,
redeem_code,
validate_license,
)
_TEMPLATE_PATH = Path(__file__).resolve().parent / "templates" / "license.html"
def _allowed_path(path: str) -> bool:
if path in ("/license", "/api/license/status", "/api/license/redeem", "/api/license/validate", "/health"):
return True
if path.startswith("/static/"):
return True
if path.startswith("/favicon"):
return True
return False
def install_license_gate(app: Flask) -> None:
"""注册 /license 与 before_request 门禁。三所 Flask 共用。"""
@app.get("/health")
def _license_health():
st = get_license_status(skip_remote=True)
return jsonify({"ok": True, "license_valid": bool(st.get("valid"))})
@app.get("/api/license/status")
def _license_status_api():
return jsonify(get_license_status())
@app.post("/api/license/redeem")
def _license_redeem_api():
data = request.get_json(silent=True) or {}
code = (data.get("code") or request.form.get("code") or "").strip()
return jsonify(redeem_code(code))
@app.post("/api/license/validate")
def _license_validate_api():
return jsonify(validate_license(force=True))
@app.route("/license", methods=["GET", "POST"])
def _license_page():
msg = ""
err = ""
if request.method == "POST":
code = (request.form.get("code") or "").strip()
result = redeem_code(code)
if result.get("ok"):
msg = result.get("message") or "激活成功"
else:
err = result.get("message") or "激活失败"
status = get_license_status()
html = _TEMPLATE_PATH.read_text(encoding="utf-8")
return render_template_string(
html,
device_id=get_device_id(),
status=status,
message=msg,
error=err,
api_url=status.get("api_url") or "",
wechat="dekun03",
)
@app.before_request
def _license_before_request():
if os.getenv("LICENSE_DISABLED", "").strip().lower() in ("1", "true", "yes", "on"):
return None
path = request.path or "/"
if _allowed_path(path):
return None
# hub bridge 内部调用:仍要求已授权(整机许可)
if is_license_valid():
return None
if path.startswith("/api/"):
return jsonify({"ok": False, "error": "license_required", "message": "请先激活许可"}), 403
return redirect("/license")
+393
View File
@@ -0,0 +1,393 @@
"""用户端许可客户端:对接 https://sq.bz121.com crypto_monitor_web)。"""
from __future__ import annotations
import hashlib
import json
import os
import platform
import threading
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from lib.paths import REPO_ROOT
DEFAULT_API_URL = "https://sq.bz121.com"
VALIDATE_INTERVAL_SEC = 3 * 24 * 3600 # 每 3 天
DEFAULT_OFFLINE_GRACE_HOURS = 72
STATE_FILENAME = "license_state.json"
LICENSE_ENV_NAME = "license.env"
_lock = threading.RLock()
_cached_device_id: str | None = None
_env_loaded = False
def _load_license_env() -> None:
global _env_loaded
if _env_loaded:
return
_env_loaded = True
path = REPO_ROOT / LICENSE_ENV_NAME
if not path.is_file():
return
try:
text = path.read_text(encoding="utf-8")
except OSError:
return
for line in text.splitlines():
raw = line.strip()
if not raw or raw.startswith("#") or "=" not in raw:
continue
key, value = raw.split("=", 1)
key = key.strip().lstrip("\ufeff")
if not key or key in os.environ:
continue
os.environ[key] = value.strip().strip('"').strip("'")
def api_base_url() -> str:
_load_license_env()
url = (os.getenv("LICENSE_API_URL") or DEFAULT_API_URL).strip().rstrip("/")
return url or DEFAULT_API_URL
def client_key() -> str:
_load_license_env()
return (os.getenv("LICENSE_CLIENT_KEY") or "").strip()
def offline_grace_hours() -> float:
_load_license_env()
raw = (os.getenv("LICENSE_OFFLINE_GRACE_HOURS") or str(DEFAULT_OFFLINE_GRACE_HOURS)).strip()
try:
return max(0.0, float(raw))
except ValueError:
return float(DEFAULT_OFFLINE_GRACE_HOURS)
def state_path() -> Path:
_load_license_env()
override = (os.getenv("LICENSE_STATE_PATH") or "").strip()
if override:
p = Path(override)
if not p.is_absolute():
p = REPO_ROOT / p
p.parent.mkdir(parents=True, exist_ok=True)
return p
data = REPO_ROOT / "data"
data.mkdir(parents=True, exist_ok=True)
return data / STATE_FILENAME
def get_device_id() -> str:
"""稳定设备指纹,长度 1664,供 redeem/validate 使用。"""
global _cached_device_id
if _cached_device_id:
return _cached_device_id
parts = [
platform.node() or "",
platform.system() or "",
platform.machine() or "",
hex(uuid.getnode()),
]
try:
machine_id = Path("/etc/machine-id")
if machine_id.is_file():
parts.append(machine_id.read_text(encoding="utf-8").strip())
except OSError:
pass
try:
win_id = Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32" / "drivers" / "etc"
parts.append(str(win_id.resolve()))
except OSError:
pass
digest = hashlib.sha256("|".join(parts).encode("utf-8", errors="ignore")).hexdigest()
_cached_device_id = digest[:40]
return _cached_device_id
def _read_state() -> dict[str, Any]:
path = state_path()
if not path.is_file():
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
except (OSError, json.JSONDecodeError):
return {}
def _write_state(data: dict[str, Any]) -> None:
path = state_path()
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(".tmp")
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
tmp.replace(path)
def _now_ts() -> float:
return time.time()
def _parse_expires_at(value: str | None) -> float | None:
if not value:
return None
s = value.strip()
if not s:
return None
try:
if s.endswith("Z"):
s = s[:-1] + "+00:00"
dt = datetime.fromisoformat(s)
if dt.tzinfo is None:
# 授权站按北京时间存,无 tz 时按 UTC+8
from datetime import timedelta, timezone as _tz
dt = dt.replace(tzinfo=_tz(timedelta(hours=8)))
return dt.timestamp()
except ValueError:
return None
def _http_json(method: str, path: str, body: dict[str, Any]) -> dict[str, Any]:
key = client_key()
if not key:
return {"ok": False, "message": "未配置 LICENSE_CLIENT_KEY(与授权站 CLIENT_API_KEY 一致)"}
url = f"{api_base_url()}{path}"
payload = json.dumps(body).encode("utf-8")
req = Request(
url,
data=payload,
method=method.upper(),
headers={
"Content-Type": "application/json",
"X-Client-Key": key,
"User-Agent": "crypto_monitor_user/license",
},
)
try:
with urlopen(req, timeout=20) as resp:
raw = resp.read().decode("utf-8", errors="replace")
data = json.loads(raw) if raw else {}
return data if isinstance(data, dict) else {"ok": False, "message": "响应格式错误"}
except HTTPError as e:
try:
detail = e.read().decode("utf-8", errors="replace")
except Exception:
detail = str(e)
return {"ok": False, "message": f"HTTP {e.code}: {detail[:200]}", "network_error": True}
except (URLError, TimeoutError, OSError) as e:
return {"ok": False, "message": f"无法连接授权服务: {e}", "network_error": True}
except json.JSONDecodeError:
return {"ok": False, "message": "授权服务返回非 JSON", "network_error": True}
def redeem_code(code: str) -> dict[str, Any]:
code = (code or "").strip().upper().replace(" ", "")
if len(code) < 8:
return {"ok": False, "message": "激活码无效"}
device_id = get_device_id()
result = _http_json("POST", "/v1/redeem", {"device_id": device_id, "code": code})
if not result.get("ok"):
return {
"ok": False,
"message": result.get("message") or "兑换失败",
"network_error": bool(result.get("network_error")),
}
now = _now_ts()
with _lock:
state = {
"device_id": device_id,
"subscription_id": result.get("subscription_id"),
"plan": result.get("plan"),
"expires_at": result.get("expires_at"),
"last_ok_at": now,
"last_validate_at": now,
"last_reason": "",
}
_write_state(state)
return {
"ok": True,
"message": result.get("message") or "激活成功",
"subscription_id": result.get("subscription_id"),
"plan": result.get("plan"),
"expires_at": result.get("expires_at"),
"device_id": device_id,
}
def validate_license(*, force: bool = False) -> dict[str, Any]:
"""联网校验;更新本地状态。force=True 忽略 3 天间隔。"""
with _lock:
state = _read_state()
sub_id = (state.get("subscription_id") or "").strip()
if not sub_id:
return {"ok": True, "valid": False, "reason": "not_activated", "message": "尚未激活"}
last_v = float(state.get("last_validate_at") or 0)
if not force and last_v and (_now_ts() - last_v) < VALIDATE_INTERVAL_SEC:
return get_license_status()
device_id = get_device_id()
result = _http_json(
"POST",
"/v1/validate",
{"device_id": device_id, "subscription_id": sub_id},
)
now = _now_ts()
if result.get("network_error"):
# 断网:在宽限期内沿用本地到期日
status = get_license_status(skip_remote=True)
status["message"] = result.get("message") or "网络异常,使用本地宽限"
status["network_error"] = True
with _lock:
st = _read_state()
st["last_network_error_at"] = now
st["last_reason"] = "network_error"
_write_state(st)
return status
valid = bool(result.get("valid"))
reason = (result.get("reason") or "").strip()
with _lock:
st = _read_state()
st["device_id"] = device_id
st["last_validate_at"] = now
if valid:
st["last_ok_at"] = now
st["last_reason"] = ""
if result.get("expires_at"):
st["expires_at"] = result.get("expires_at")
if result.get("plan"):
st["plan"] = result.get("plan")
else:
st["last_reason"] = reason or "invalid"
if reason in ("expired", "device_revoked", "subscription_not_found", "subscription_inactive"):
# 明确失效:保留字段便于页面展示,但 valid=False
pass
_write_state(st)
return {
"ok": True,
"valid": valid,
"reason": reason if not valid else "",
"expires_at": result.get("expires_at") or state.get("expires_at"),
"plan": result.get("plan") or state.get("plan"),
"device_id": device_id,
"subscription_id": sub_id,
"message": "有效" if valid else (reason or "无效"),
}
def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
"""本地状态(可触发到期判断);不主动联网除非 skip_remote=False 且到期需刷新。"""
with _lock:
state = _read_state()
device_id = get_device_id()
sub_id = (state.get("subscription_id") or "").strip()
if not sub_id:
return {
"ok": True,
"valid": False,
"reason": "not_activated",
"message": "尚未激活",
"device_id": device_id,
"api_url": api_base_url(),
}
exp_ts = _parse_expires_at(state.get("expires_at"))
now = _now_ts()
if exp_ts is not None and now > exp_ts:
return {
"ok": True,
"valid": False,
"reason": "expired",
"message": "许可已过期",
"expires_at": state.get("expires_at"),
"plan": state.get("plan"),
"device_id": device_id,
"subscription_id": sub_id,
"api_url": api_base_url(),
}
reason = (state.get("last_reason") or "").strip()
if reason in ("expired", "device_revoked", "subscription_not_found", "subscription_inactive"):
return {
"ok": True,
"valid": False,
"reason": reason,
"message": reason,
"expires_at": state.get("expires_at"),
"plan": state.get("plan"),
"device_id": device_id,
"subscription_id": sub_id,
"api_url": api_base_url(),
}
last_ok = float(state.get("last_ok_at") or 0)
grace = offline_grace_hours() * 3600
if last_ok and grace > 0 and (now - last_ok) > grace:
# 超过离线宽限且长时间未成功校验
last_v = float(state.get("last_validate_at") or 0)
if last_v and (now - last_v) >= VALIDATE_INTERVAL_SEC and not skip_remote:
return validate_license(force=True)
if (now - last_ok) > grace:
return {
"ok": True,
"valid": False,
"reason": "offline_grace_exceeded",
"message": "超过离线宽限期,请联网校验",
"expires_at": state.get("expires_at"),
"plan": state.get("plan"),
"device_id": device_id,
"subscription_id": sub_id,
"api_url": api_base_url(),
}
return {
"ok": True,
"valid": True,
"reason": "",
"message": "已授权",
"expires_at": state.get("expires_at"),
"plan": state.get("plan"),
"device_id": device_id,
"subscription_id": sub_id,
"last_ok_at": last_ok or None,
"api_url": api_base_url(),
}
def is_license_valid() -> bool:
status = get_license_status()
if status.get("valid"):
# 到期需周期性联网
last_v = 0.0
with _lock:
st = _read_state()
last_v = float(st.get("last_validate_at") or 0)
if last_v and (_now_ts() - last_v) >= VALIDATE_INTERVAL_SEC:
status = validate_license(force=True)
return bool(status.get("valid"))
# 未激活或已失效:尝试若有 subscription 则强制校验一次(避免本地过期标志陈旧)
with _lock:
st = _read_state()
if (st.get("subscription_id") or "").strip() and status.get("reason") not in (
"not_activated",
"expired",
"device_revoked",
):
status = validate_license(force=True)
return bool(status.get("valid"))
return False
def ensure_license_or_raise() -> None:
if not is_license_valid():
raise RuntimeError("license_required")
+100
View File
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>软件授权</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: #0a0a10;
color: #e8e8f0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.box {
width: 100%;
max-width: 440px;
background: #12121a;
border: 1px solid #2a2a3a;
border-radius: 14px;
padding: 28px 24px;
}
h1 {
font-size: 1.35rem;
margin-bottom: 8px;
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.muted { color: #9aa0b4; font-size: 0.9rem; margin-bottom: 18px; line-height: 1.5; }
.row { margin-bottom: 14px; }
label { display: block; font-size: 0.85rem; color: #b8bfd4; margin-bottom: 6px; }
.id {
word-break: break-all;
background: #0c0c14;
border: 1px solid #2a2a3a;
border-radius: 8px;
padding: 10px 12px;
font-family: ui-monospace, Consolas, monospace;
font-size: 0.85rem;
}
input[type=text] {
width: 100%;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid #2a2a3a;
background: #0c0c14;
color: #fff;
font-size: 1rem;
}
button {
width: 100%;
margin-top: 8px;
padding: 11px 14px;
border: 0;
border-radius: 8px;
background: linear-gradient(90deg, #3a8dff, #6b4dff);
color: #fff;
font-size: 1rem;
cursor: pointer;
}
.ok { color: #5ddea8; margin: 10px 0; font-size: 0.9rem; }
.err { color: #ff7b8a; margin: 10px 0; font-size: 0.9rem; }
.meta { margin-top: 16px; font-size: 0.85rem; color: #9aa0b4; line-height: 1.6; }
a { color: #7ec8ff; }
</style>
</head>
<body>
<div class="box">
<h1>软件授权</h1>
<p class="muted">复制下方设备 ID,联系微信 <strong>{{ wechat }}</strong> 购买激活码后粘贴兑换。授权站:{{ api_url }}</p>
{% if message %}<p class="ok">{{ message }}</p>{% endif %}
{% if error %}<p class="err">{{ error }}</p>{% endif %}
<div class="row">
<label>设备 ID</label>
<div class="id" id="deviceId">{{ device_id }}</div>
</div>
<form method="post" action="/license">
<div class="row">
<label>激活码</label>
<input type="text" name="code" placeholder="粘贴激活码" autocomplete="off" required>
</div>
<button type="submit">兑换激活</button>
</form>
<div class="meta">
状态:{% if status.valid %}已授权{% else %}未授权{% endif %}
{{ status.message or status.reason or '—' }}<br>
套餐:{{ status.plan or '—' }} 到期:{{ status.expires_at or '—' }}
</div>
</div>
</body>
</html>