授权模型切换为个人副本签发;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 项全绿
This commit is contained in:
2026-10-09 21:24:25 +08:00
parent 5889d7a150
commit 26a83df3c4
20 changed files with 2760 additions and 22 deletions
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""脱敏包推送 —— 把 share/pending/ 的包 git commit+push 到共建远端。
用法:python share_push.py [--root 技能根目录]
远端取自 share/config.json 的 github_remote / gitea_remote(占位为空则跳过,
由授权方填写;亦可用环境变量 ZIPING_SHARE_GITHUB_REMOTE /
ZIPING_SHARE_GITEA_REMOTE 覆盖)。两个远端逐个尝试,至少一个成功即算成功。
推送成功后:调 _license_gate.grant_uses(root, 10, 盘hash) 计励(同一盘只计一次),
并把该包移入 share/pushed/。推送失败保留 pending,退出码 3。
无 git 或无网络安静失败(不打栈)。
退出码:0 至少一包推送成功或无包可推;2 输入不可用;3 推送失败。
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import _license_gate as gate
GRANT_USES = 10 # 每盘推送成功的计励次数
ENV_GITHUB = "ZIPING_SHARE_GITHUB_REMOTE"
ENV_GITEA = "ZIPING_SHARE_GITEA_REMOTE"
_GIT_ENV = {**os.environ, "GIT_TERMINAL_PROMPT": "0", "GIT_ASKPASS": "true"}
def _git(*args: str, cwd: str | None = None) -> subprocess.CompletedProcess:
return subprocess.run(["git", *args], cwd=cwd, env=_GIT_ENV,
capture_output=True, text=True, timeout=60)
def _push_to_remote(remote: str, pkg: Path, workdir: str) -> bool:
"""克隆远端 → 放入脱敏包 → commit+push。任一步失败返回 False(安静)。"""
try:
if _git("clone", "--depth", "50", remote, workdir).returncode != 0:
return False
dst = Path(workdir) / pkg.name
shutil.copy2(pkg, dst)
if _git("add", pkg.name, cwd=workdir).returncode != 0:
return False
commit = _git("-c", "user.name=ziping-share",
"-c", "user.email=ziping-share@localhost",
"commit", "--allow-empty", "-m", f"share: {pkg.stem}",
cwd=workdir)
if commit.returncode != 0:
return False
return _git("push", "origin", "HEAD:main", cwd=workdir).returncode == 0
except (OSError, subprocess.SubprocessError):
return False
def _remotes(root: Path) -> list[tuple[str, str]]:
cfg = {}
cfg_path = root / "share" / "config.json"
if cfg_path.is_file():
try:
cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
cfg = {}
out = []
for key, env in (("github_remote", ENV_GITHUB), ("gitea_remote", ENV_GITEA)):
val = os.environ.get(env) or str(cfg.get(key) or "").strip()
if val:
out.append((key, val))
return out
def main() -> int:
ap = argparse.ArgumentParser(prog="share_push.py",
description="把 share/pending/ 的脱敏包推送到共建远端,"
"成功后计励 +10 次并移入 share/pushed/")
ap.add_argument("--root", default=None,
help="技能根目录(默认取脚本所在技能根,测试覆盖用)")
a = ap.parse_args()
root = (Path(a.root).expanduser().resolve() if a.root
else Path(__file__).resolve().parents[1])
if shutil.which("git") is None:
print("ERROR: 未找到 git,无法推送;pending 保留。", file=sys.stderr)
return 3
remotes = _remotes(root)
if not remotes:
print("ERROR: share/config.json 未配置远端(github_remote/gitea_remote "
"由授权方填写);pending 保留。", file=sys.stderr)
return 3
pending = root / "share" / "pending"
pushed = root / "share" / "pushed"
packages = sorted(pending.glob("*.json")) if pending.is_dir() else []
if not packages:
print("share/pending/ 无待推送脱敏包。")
return 0
pushed.mkdir(parents=True, exist_ok=True)
n_ok = 0
for pkg in packages:
chart_hash = pkg.stem
ok_remote = None
for name, remote in remotes:
with tempfile.TemporaryDirectory(prefix="ziping-share-") as td:
if _push_to_remote(remote, pkg, td):
ok_remote = name
break
if ok_remote is None:
print(f"FAIL {pkg.name}:所有远端推送失败(无网络或远端不可达),"
f"pending 保留。", file=sys.stderr)
continue
granted = gate.grant_uses(root, GRANT_USES, chart_hash)
shutil.move(str(pkg), str(pushed / pkg.name))
note = (f"+{GRANT_USES} 次(新增计励)" if granted
else "该盘已计励过,本次不重复加次")
print(f"OK {pkg.name} → {ok_remote};{note}")
n_ok += 1
return 0 if n_ok else 3
if __name__ == "__main__":
raise SystemExit(main())