feat: accept CLIENT_API_KEY on /license page.

Users can paste the key from the seller instead of editing license.env; it persists in license_state.json.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-17 17:20:47 +08:00
parent 863da10d1f
commit 8eefc6cfd5
6 changed files with 91 additions and 32 deletions
+54 -12
View File
@@ -63,7 +63,25 @@ def api_base_url() -> str:
def client_key() -> str:
_load_license_env()
return (os.getenv("LICENSE_CLIENT_KEY") or "").strip()
env_key = (os.getenv("LICENSE_CLIENT_KEY") or "").strip()
if env_key:
return env_key
with _lock:
st = _read_state()
return (st.get("client_api_key") or "").strip()
def set_client_api_key(key: str) -> dict[str, Any]:
"""保存用户在授权页填写的 CLIENT_API_KEY(写入 license_state.json)。"""
key = (key or "").strip()
if len(key) < 8:
return {"ok": False, "message": "CLIENT_API_KEY 太短"}
with _lock:
st = _read_state()
st["client_api_key"] = key
_write_state(st)
os.environ["LICENSE_CLIENT_KEY"] = key
return {"ok": True, "message": "CLIENT_API_KEY 已保存"}
def offline_grace_hours() -> float:
@@ -162,7 +180,7 @@ def _parse_expires_at(value: str | None) -> float | 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。请在仓库根目录复制 license.env.example 为 license.env,填入与授权站相同的 CLIENT_API_KEY,然后重启服务"}
return {"ok": False, "message": "未配置 CLIENT_API_KEY。请在软件授权页填写卖家提供的密钥,或在 license.env 中设置 LICENSE_CLIENT_KEY"}
url = f"{api_base_url()}{path}"
payload = json.dumps(body).encode("utf-8")
req = Request(
@@ -192,10 +210,19 @@ def _http_json(method: str, path: str, body: dict[str, Any]) -> dict[str, Any]:
return {"ok": False, "message": "授权服务返回非 JSON", "network_error": True}
def redeem_code(code: str) -> dict[str, Any]:
def redeem_code(code: str, client_api_key: str | None = None) -> dict[str, Any]:
code = (code or "").strip().upper().replace(" ", "")
if len(code) < 8:
return {"ok": False, "message": "激活码无效"}
if client_api_key is not None and str(client_api_key).strip():
saved = set_client_api_key(str(client_api_key))
if not saved.get("ok"):
return saved
if not client_key():
return {
"ok": False,
"message": "请先填写 CLIENT_API_KEY(向卖家索取,与授权站后台「系统设置」中的密钥一致)",
}
device_id = get_device_id()
result = _http_json("POST", "/v1/redeem", {"device_id": device_id, "code": code})
if not result.get("ok"):
@@ -206,15 +233,18 @@ def redeem_code(code: str) -> dict[str, Any]:
}
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": "",
}
state = _read_state()
state.update(
{
"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,
@@ -294,6 +324,8 @@ def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
with _lock:
state = _read_state()
device_id = get_device_id()
key = client_key()
key_mask = (key[:4] + "" + key[-4:]) if len(key) >= 12 else (("已配置" if key else ""))
sub_id = (state.get("subscription_id") or "").strip()
if not sub_id:
return {
@@ -303,6 +335,8 @@ def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
"message": "尚未激活",
"device_id": device_id,
"api_url": api_base_url(),
"has_client_key": bool(key),
"client_key_masked": key_mask,
}
exp_ts = _parse_expires_at(state.get("expires_at"))
@@ -318,6 +352,8 @@ def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
"device_id": device_id,
"subscription_id": sub_id,
"api_url": api_base_url(),
"has_client_key": bool(key),
"client_key_masked": key_mask,
}
reason = (state.get("last_reason") or "").strip()
@@ -332,6 +368,8 @@ def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
"device_id": device_id,
"subscription_id": sub_id,
"api_url": api_base_url(),
"has_client_key": bool(key),
"client_key_masked": key_mask,
}
last_ok = float(state.get("last_ok_at") or 0)
@@ -352,6 +390,8 @@ def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
"device_id": device_id,
"subscription_id": sub_id,
"api_url": api_base_url(),
"has_client_key": bool(key),
"client_key_masked": key_mask,
}
return {
@@ -365,6 +405,8 @@ def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
"subscription_id": sub_id,
"last_ok_at": last_ok or None,
"api_url": api_base_url(),
"has_client_key": bool(key),
"client_key_masked": key_mask,
}