Files
moxiaonian 1a0eed1999 规则库 3.8.1 + 排盘修复 + 评测体系
- 规则库 3.6→3.8.1(542→548 条):剪枝上锁、R-杂-38 从格临界双轨、
  R-事-10~13 冲刑方向判别、R-禁-23 自造必要条件之禁
- 排盘修复随 3.6.2 已发:港澳时区夏令时双向误拨、晚子时双重换日
- 新增 评测/ 目录(BaziQA 题答分离、判分工具、记分卡、with/without-skill 对照)
  与 scripts/query_rules.py 条款检索;ZIPEC_EVAL 评测额度豁免
- manifest 重建并重新签名;VERSION 保持 3.6.2,不发版不打 tag
2026-10-13 12:34:36 +08:00

164 lines
6.2 KiB
Python
Raw Permalink 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 os
import secrets
from datetime import date
from pathlib import Path
_ENABLED = True # 软锁总开关:False = 完全放行
# 评测豁免:环境变量 ZIPEC_EVAL=1 时 check/consume 直接放行——
# 跑测试集复测不签发、不扣次、不创建任何文件(见 评测/测试规则.md)。
_EVAL_ENV = "ZIPEC_EVAL"
_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
if os.environ.get(_EVAL_ENV) == "1":
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
if os.environ.get(_EVAL_ENV) == "1":
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