Files
ZiPingEightChar/scripts/_license_gate.py
T
moxiaonian 26a83df3c4 授权模型切换为个人副本签发;share 脱敏共建;自更新通道
- 公众版不再共享 1000 次授权池:移除 data/.license.json,
  改置 data/.public_edition.json 标记,首次排盘本机签发个人副本
  (初始 20 次、不限期);推送脱敏档案每盘 +10 次(同盘只计一次)
- 新增 share/ 共建:结构化脱敏导出(share_export.py)+
  双远端推送(share_push.py)+ 隐私协议(share/README.md);
  推送前必须展示脱敏包全文并获用户授权
- 新增自更新:VERSION + manifest.json(SHA256 清单)+
  scripts/self_update.py(24h 缓存检查、先验后换、保留授权与档案);
  排盘成功后 stderr 提示新版本
- SKILL.md 增「档案共建(share/)」「版本与自更新」两节;
  LICENSE.md 增第六条(档案权属与撤回)
- 测试 100 项全绿
2026-10-09 21:24:25 +08:00

155 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""软授权锁(防君子不防小人)。
总开关就是下面的 `_ENABLED` 一行变量:置 False 则完全放行。
本锁只是授权边界的可见表达,真正的约束在 LICENSE.md 的法律条款——
绕过、篡改、移除软锁或水印,即构成对授权的违反。
母版(技能开发目录)无 `data/.license.json` 且无公众版标记,本模块全部放行;
由 `tools/make_license_copy.py` 生成的定制副本带该状态文件,
按剩余次数与到期日限制使用,并以签名防止手工篡改状态文件。
公众版副本(`tools/make_license_copy.py --public-edition`)不带授权状态文件,
只带标记 `data/.public_edition.json`;首次 check 时就地签发个人副本
(初始 20 次、不限期、授权对象 public-personal),之后按定制副本同样校验。
退出码 4 = 授权不可用(次数用尽 / 已过期 / 状态文件被篡改)。
"""
from __future__ import annotations
import hashlib
import json
import secrets
from datetime import date
from pathlib import Path
_ENABLED = True # 软锁总开关:False = 完全放行
_SALT = "lingyao-miaojie-ziping-eightchar"
_COMPANY = "四川灵爻妙解文化传播有限公司"
STATE_REL = Path("data") / ".license.json"
PUBLIC_MARKER_REL = Path("data") / ".public_edition.json"
# 公众版首启签发的个人副本参数
PUBLIC_LICENSEE = "public-personal"
PUBLIC_INITIAL_USES = 20
def _state_path(root: Path) -> Path:
return root / STATE_REL
def _sig(st: dict) -> str:
raw = "|".join([
str(st["copy_id"]), str(st["licensee"]), str(st["max_uses"]),
str(st["remaining_uses"]), str(st["expires_at"]), _SALT,
])
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
def _load(root: Path) -> dict | None:
p = _state_path(root)
if not p.is_file():
return None
try:
return json.loads(p.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
# 状态文件损坏按篡改变体处理:拒跑
return {"copy_id": "?", "licensee": "?", "remaining_uses": 0,
"max_uses": 0, "expires_at": None, "sig": ""}
def _deny(reason: str, st: dict) -> str:
return (f"授权不可用:{reason}。"
f"(副本编号 {st.get('copy_id', '?')},授权对象:{st.get('licensee', '?')})"
f"本技能为测试授权副本,禁止商业用途与个人传播;"
f"如需续期或正式授权,请联系{_COMPANY}。")
def _issue_public_personal(root: Path) -> None:
"""公众版首启:就地签发个人副本(初始 20 次、不限期)。"""
write_state(root,
copy_id=f"LY-{date.today():%Y%m%d}-{secrets.token_hex(2).upper()}",
licensee=PUBLIC_LICENSEE,
max_uses=PUBLIC_INITIAL_USES,
expires_at=None)
def check(root: Path) -> str | None:
"""返回 None 表示放行;返回字符串表示拒跑原因(由调用方打印到 stderr,退出码 4)。"""
if not _ENABLED:
return None
st = _load(root)
if st is None:
if (root / PUBLIC_MARKER_REL).is_file():
_issue_public_personal(root) # 公众版首启:就地签发个人副本
st = _load(root)
else:
return None # 母版无状态文件,不锁
if st is None: # 签发失败(如磁盘只读)按母版放行,不阻断使用
return None
if st.get("sig") != _sig(st):
return _deny("授权状态校验失败(授权文件被改动过)", st)
exp = st.get("expires_at")
if exp and date.today() > date.fromisoformat(exp):
return _deny(f"授权已于 {exp} 到期", st)
if st.get("remaining_uses", 0) <= 0:
return _deny("本副本的使用次数已用尽", st)
return None
def consume(root: Path) -> None:
"""成功排盘一次后扣减一次。仅在 check 放行后调用。"""
if not _ENABLED:
return
st = _load(root)
if st is None:
return
st["remaining_uses"] = max(0, int(st.get("remaining_uses", 0)) - 1)
st["sig"] = _sig(st)
_state_path(root).write_text(
json.dumps(st, ensure_ascii=False, indent=2), encoding="utf-8")
def write_state(root: Path, *, copy_id: str, licensee: str,
max_uses: int, expires_at: str | None = None) -> dict:
"""生成授权状态文件(供 tools/make_license_copy.py 调用,签名算法单点在此)。"""
st = {
"copy_id": copy_id,
"licensee": licensee,
"issued_at": date.today().isoformat(),
"expires_at": expires_at,
"max_uses": max_uses,
"remaining_uses": max_uses,
"granted_archives": [],
}
st["sig"] = _sig(st)
p = _state_path(root)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(json.dumps(st, ensure_ascii=False, indent=2), encoding="utf-8")
return st
def grant_uses(root: Path, n: int, archive_hash: str) -> bool:
"""脱敏档案共建的计励:推送成功后给本副本加 n 次。
同一盘(archive_hash)只计一次:已在已计励列表中则返回 False 且不加次。
母版(无授权状态文件)不计励,直接返回 True。
状态文件签名校验失败(被篡改)时拒绝计励,返回 False。
"""
if not _ENABLED:
return True
st = _load(root)
if st is None:
return True # 母版无锁,不计
if st.get("sig") != _sig(st):
return False
granted = st.setdefault("granted_archives", [])
if archive_hash in granted:
return False
st["remaining_uses"] = int(st.get("remaining_uses", 0)) + int(n)
granted.append(archive_hash)
st["sig"] = _sig(st)
_state_path(root).write_text(
json.dumps(st, ensure_ascii=False, indent=2), encoding="utf-8")
return True