diff --git a/VERSION b/VERSION index 40c341b..9575d51 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.6.0 +3.6.1 diff --git a/manifest.json b/manifest.json index d1f2a7e..f3ab8da 100644 --- a/manifest.json +++ b/manifest.json @@ -4,11 +4,11 @@ "LICENSE.md": "6b839540e0fd4ab75530ec31fc5c3cbc3c2d5de5fe85068ef1c0901e54be9315", "README.md": "adb4481f20360c66e67fd89076c1535c577cdf85f34122d2dac1ff727dd81c38", "SKILL.md": "720aa2de6e7e6f65c44f63421f0aece401146921e54a6c99f24db1af14241813", - "VERSION": "138203e3f604a08fad0c4d0a297efe2c486219820dcdd053e72b7d843504c7a1", + "VERSION": "de58d841bd709aa85a66ecc500dfbb48e7c207e84c399e30e751e8ccad6e8c9b", "data/.public_edition.json": "e8692c45d512c78903fc89a924d0e6d4dc9b95a78ca6b359bb7a5ef5c946ffee", "data/cities.json": "00f17905c8843218162e25238a78aeb4c4a3bb5424b611cc30ed9e436db31854", "memory/README.md": "a979f985dd87e87bfca49676ebf10a537d299b1eeebac121c506c320c88a3afd", - "pyproject.toml": "d71541e952e8b1650a157bf17d2bed9946052df9f6647032218ca272973d93b4", + "pyproject.toml": "b202c2ca784d501959d3dc4d7024851de8b66b15a7e3368b81566b2e045601b4", "requirements.txt": "46a4c971b192cbf2c4972dd0f420e7928b7e44cd3cb9f7dc5a36efb321ab6fd6", "rules/00_总纲_取格.md": "829a35454f5c91966ecafe4dd2113b913588933efe08d0e1c127ae82ccec0da8", "rules/11_正官格.md": "385193d93dfba89a935ca8907098f70548ad1fc12ab6a07a5280fd04224c83b6", @@ -30,7 +30,7 @@ "rules/96_六亲.md": "96cf362bd180558352d0181401ac8e297ac89c09d91d1098972a9d1c40468198", "rules/97_女命.md": "52962aa666faa3fa2a5175d352380df14f54fefb8f57cfe0761779ad51e18696", "rules/99_附录_干支通例.md": "da906502391833eb1744ff0145a03c7d1e10781ac7156034632dc5994da0d2f4", - "rules/README.md": "27118f8338549ae4f92d1c9ed66e6bea82e028f2a5f68769bc43e08770857b51", + "rules/README.md": "e08256c5fe13683bab93076b19fde18c1c1455053dec83f37121e4718817d489", "rules/sources/README.md": "183ba554b1d89c16b19fb65ac8a4d0614069c865676fb4c1bd20d95b5581c209", "rules/sources/三命通会/00_卷一·原造化之始.md": "6329058dd08aed5f41d1111da99027cac38761aeea7120a84eff02683426294d", "rules/sources/三命通会/01_卷一·论五行生成.md": "c9db87683a6d3f9b7eea665e3814e383e11a69b608114d9317beb2a2c56825f8", @@ -566,6 +566,7 @@ "scripts/_license_gate.py": "4f3a24685c86abc6c66a158b441b89e2738bd14f5d63e8bdd41489654a29e837", "scripts/_memory_store.py": "690e088a208ee5aeb9f66e6d4b5d4342f44bfc852bad51870cdfe397fbdf7660", "scripts/_runtime_guard.py": "c4f9d00acbb5b2ba49680b23ed4f846c60211e67b9d58950079b3e8909dcfe81", + "scripts/_update_sig.py": "9bb3521b950ab834c770e313a98c9cc8f7e997997540258724505af774a4abe6", "scripts/astro/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "scripts/astro/day_boundary.py": "b5a24b3937fbcd9c278331b5b19f8cf708acc987f6f6f97f346524786735af9a", "scripts/astro/equation_of_time.py": "ee9331022b4ed35d407664e97b5ce9ce99167a06ce7cb8c9ffba5497a437da8e", @@ -588,10 +589,10 @@ "scripts/render/assets/lingyao-logo.png": "3d63f71ff9af61a443708f2665d1331737bb567a4e731d049676dfeb4710e323", "scripts/render/assets/logo-font.ttf": "0b3a28b91d30276a68501a837ce7c1874274276c21793957b83f1efbb4a9264a", "scripts/render/assets/qun-qrcode.jpg": "437854ff4ea6d2b3452b16371afbba1b9bb97e1f2c7fdbc77580ae2cceb3cbaf", - "scripts/render/html.py": "3196b808d83b876ff1d4b7ea13bbb97f27154bfcd27b491cd10b2e4c0c254874", + "scripts/render/html.py": "f6054c3fddc9223e32061bbc2bb88c01b9888606baf118ab3ddfb1d11f11ec14", "scripts/render/markdown.py": "4b2bcc3fc6581728d74bbaa357bf1db2e8aec1d3ec54c2c27ca645b6c5179977", - "scripts/render/templates/paipan.html": "aecc6a7ec5d6dcb567e56a986dc803678838f12551828ff58ae313ae7a6499a5", - "scripts/self_update.py": "ca8cf55dffd805a3643e71167912e6468f33e1b7fe0a270ec263f6d9bf949bb8", + "scripts/render/templates/paipan.html": "62fc3fcf355089cbedfc9e7b39fae54bb93e91cab73be3c376145b2643853a28", + "scripts/self_update.py": "c7bbb58e750fdbbecad00936948e37d7ff7a47c57e22d116c537c104aa74e8de", "scripts/share_export.py": "eab38048e3737de1eb8731e6f4860f4061d29dedb9e9f3a1c9d9ac44ec200c20", "scripts/share_push.py": "307e83747b8d244f20632a198b7bfba51352a6b6bf0a8f98b73c904e719a6b89", "share/README.md": "adccd0f7691bedd64441b11cdd8ce66e20adce55fe4d8de1333f531b689f82b8", @@ -603,16 +604,18 @@ "tests/test_day_boundary.py": "0e6c9ee32177a178f21a7812f83032abe4d1f585de48d07d21e2b432bdbcbd04", "tests/test_e2e.py": "e324930fad582dc38618fa3599425bb48ec718beaf426efcb76531911c9464fa", "tests/test_equation_of_time.py": "8a3a1baf7e61c42015405cc80fc0bdf4fd9bc732d440bc156f9c92c94e30f790", - "tests/test_html.py": "29fbab116aaa48ef9b97f26b6eec6c22157cd8c6af9da61943e20dd93157e49d", + "tests/test_html.py": "d40f4558ddfda156706a0c3d4c52c794fbc1e59427acdc93bcdad7df34217638", "tests/test_license_gate.py": "ee44226b75620b3965ba52cc2a0cc5404fbff37822fd96a6faf2aed89d07d47e", "tests/test_luck.py": "821f98bc19ab1e920353de58c6ffacac5d757c887c690a2d596f51c1485b21ea", "tests/test_memory.py": "068241927df08301806451ff6e66206ceb9d439d95197b3506fe519d892c28ed", "tests/test_runtime_guard.py": "7fd7e2f334433432452ea68fde30df1698def2126a1920f8da6d6aab236c1caf", - "tests/test_self_update.py": "183c2fb82d2d20285941b6c75a1890c4417d400c3c7e7d5f8a9680b4fa7bf76c", + "tests/test_self_update.py": "87a0fdaf28132e70fe407a552ff77f5911e13e67ca1d568581a273eaba875712", "tests/test_share.py": "7938344415cbba95f683b2a09ecec9af28d00658f475ff0a56563dd13beff630", "tests/test_shensha.py": "2aaa181279739356828f05a2175027b0ae711b322b4bd0e60d5c5e62538bcac8", "tests/test_solar_time.py": "f072e5ea82fd0cb331c8a4fdc6dce08148cd9ac81ef9fe4506196fa68a42d1e4", + "tests/test_sync_tool.py": "67395f121cfa0092af7edb579dac54a07690b989eb82bb16c6ea815325e47ce3", "tests/test_timezone.py": "3698878999d232bd431d6908c42fe8f4845c696da6085731abaf2f74017d722d", + "tests/test_update_sig.py": "3bfd6ce28a699afe233fe51d9d9c7aacc308f54a4dcb9f9e7ecd272bb0a9b5a2", "tests/test_version_sentinel.py": "9b2bc603a2e86aa33d904f1381ecd8ab41eb0e759803ec47122d662c10a7df83", "vendor/VENDOR.md": "dbf017616ac2fda4fef56ce95e3610e77457108b7fa426136fd25764d78fa3c8", "vendor/tyme4py.LICENSE": "e017c12d3476152144886c06e12b67277bb64e922b88c902b76ac02a84ea1c9d", @@ -1276,5 +1279,5 @@ "vendor/tzdata/zoneinfo/zonenow.tab": "7f9d1c11445af7a17862c20a9081226d31a09a1730e270ff59a34c8d2b4c7f7f", "vendor/tzdata/zones": "28d8f3eb25839095fa8a209997bb20e822ebfbebe21e60edc483a3d5e20dc881" }, - "version": "3.6.0" + "version": "3.6.1" } diff --git a/manifest.json.sig b/manifest.json.sig new file mode 100644 index 0000000..940eaf6 --- /dev/null +++ b/manifest.json.sig @@ -0,0 +1 @@ +/hVDbf9RW2/oWP5z1nhGyc6/Ip76Inw4mLKaRtsojbuDS/tAaY9km8phEBGgC4T8qunh/FD8qc4dcVHk+iJfCA== diff --git a/pyproject.toml b/pyproject.toml index 78a889b..d7c06c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ZiPingEightChar" -version = "3.6.0" +version = "3.6.1" requires-python = ">=3.10" # zoneinfo 需 3.9+,dataclass 语法需 3.10 dependencies = [ "tyme4py==1.5.0", # 精确锁定:历表数据跨版本会变,锁版本 = 锁可复现性 diff --git a/rules/README.md b/rules/README.md index bdefce3..2b126ff 100644 --- a/rules/README.md +++ b/rules/README.md @@ -105,7 +105,9 @@ rules/ - **3.6**(2026-10-10)。文件头注释的 `rules_version` 标记该文件的**格式版本**;库版本以本文件为准。 ### 3.6 相对 3.5: -- 询问契约改对话式(规则条数不变,542 条、20 文件)——SKILL.md Intake 节改「对话式询问(强制)」(禁用结构化问卷/点选工具与正文编号选项清单,一次最多 1~2 问、先铺垫再问),断运交互节同步;`90_行运.md` R-运-33 措辞随之修订——原「具备询问工具时必须用询问工具,否则以正文编号选项代替」「核对用结构化选项、多锚点年合成一次提问」改为对话式口径(普通对话消息、一次最多一两个锚点、先铺垫再问、答完再定下一个);核对前置的内部程序(场景分流、锚点选取、候选排序、证伪回查、收敛后一次性输出)不变。 +- 询问契约改对话式(规则条数不变,542 条、20 文件)——SKILL.md Intake 节改「对话式询问(强制)」(禁用结构化问卷/点选工具与正文编号选项清单,一次最多 1~2 问、先铺垫再问),断运交互节同步; +- `90_行运.md` R-运-33 措辞随之修订——原「具备询问工具时必须用询问工具,否则以正文编号选项代替」「核对用结构化选项、多锚点年合成一次提问」改为对话式口径(普通对话消息、一次最多一两个锚点、先铺垫再问、答完再定下一个); +- 核对前置的内部程序(场景分流、锚点选取、候选排序、证伪回查、收敛后一次性输出)不变。 ### 3.5 相对 3.4: - description 改写为按问题域与使用场景触发;新增 R-禁-22 声明式偏离通道;R-运-31 事类映射表改开放清单措辞;规则计数相应更新(541 → 542 条,20 文件不变)。 diff --git a/scripts/_update_sig.py b/scripts/_update_sig.py new file mode 100644 index 0000000..22130b7 --- /dev/null +++ b/scripts/_update_sig.py @@ -0,0 +1,128 @@ +"""纯 Python ed25519(RFC 8032)签名与验签,零依赖,仅用标准库。 + +self_update.py --apply 用其中的 verify() 校验 manifest.json 签名; +tools/sign_manifest.py 用 sign() 生成签名。验签性能不在关键路径上 +(每次更新只验一次),实现取教科书式的扩展坐标点运算,求正确不求快。 + +密钥编码:私钥 = 32 字节种子(hex/base64 存储);公钥 = 32 字节 RFC 8032 +标准压缩编码;签名 = 64 字节 R‖S。 +""" +import hashlib + +# 域参数:GF(2^255 - 19),曲线 -x² + y² = 1 + d·x²·y² +_P = 2 ** 255 - 19 +_Q = 2 ** 252 + 27742317777372353535851937790883648493 # 群的阶 +_D = (-121665 * pow(121666, _P - 2, _P)) % _P +_I = pow(2, (_P - 1) // 4, _P) # sqrt(-1) + + +def _xrecover(y: int) -> int: + xx = (y * y - 1) * pow(_D * y * y + 1, _P - 2, _P) % _P + x = pow(xx, (_P + 3) // 8, _P) + if (x * x - xx) % _P != 0: + x = x * _I % _P + if x & 1: + x = _P - x + return x + + +_BY = (4 * pow(5, _P - 2, _P)) % _P +_BX = _xrecover(_BY) +# 基点(扩展坐标 x, y, z, t,x=X/Z, y=Y/Z, t=XY/Z) +_B = (_BX, _BY, 1, _BX * _BY % _P) +_IDENTITY = (0, 1, 1, 0) + + +def _point_add(P, Q): + x1, y1, z1, t1 = P + x2, y2, z2, t2 = Q + a = (y1 - x1) * (y2 - x2) % _P + b = (y1 + x1) * (y2 + x2) % _P + c = 2 * _D * t1 * t2 % _P + d = 2 * z1 * z2 % _P + e, f, g, h = b - a, d - c, d + c, b + a + return (e * f % _P, g * h % _P, f * g % _P, e * h % _P) + + +def _scalarmult(P, e: int): + Q = _IDENTITY + while e > 0: + if e & 1: + Q = _point_add(Q, P) + P = _point_add(P, P) + e >>= 1 + return Q + + +def _encode_point(P) -> bytes: + x, y, z, _t = P + zi = pow(z, _P - 2, _P) + x, y = x * zi % _P, y * zi % _P + return (y | ((x & 1) << 255)).to_bytes(32, "little") + + +def _decode_point(s: bytes): + if len(s) != 32: + raise ValueError("点编码长度须为 32 字节") + y = int.from_bytes(s, "little") & ((1 << 255) - 1) + if y >= _P: + raise ValueError("点编码 y 坐标越界") + x = _xrecover(y) + if (x & 1) != (s[31] >> 7): + x = _P - x + if (-x * x + y * y - 1 - _D * x * x * y * y) % _P != 0: + raise ValueError("点不在曲线上") + return (x, y, 1, x * y % _P) + + +def _hint(m: bytes) -> int: + return int.from_bytes(hashlib.sha512(m).digest(), "little") + + +def _secret_expand(seed: bytes): + if len(seed) != 32: + raise ValueError("私钥种子须为 32 字节") + h = hashlib.sha512(seed).digest() + a = int.from_bytes(h[:32], "little") + a &= (1 << 254) - 8 + a |= 1 << 254 + return a, h[32:] + + +def publickey(seed: bytes) -> bytes: + """32 字节种子 → 32 字节公钥。""" + a, _prefix = _secret_expand(seed) + return _encode_point(_scalarmult(_B, a)) + + +def sign(seed: bytes, msg: bytes) -> bytes: + """RFC 8032 确定性签名:64 字节 R‖S。仅发版工具(tools/)使用。""" + a, prefix = _secret_expand(seed) + A = publickey(seed) + r = _hint(prefix + msg) % _Q + R = _encode_point(_scalarmult(_B, r)) + S = (r + _hint(R + A + msg) * a) % _Q + return R + S.to_bytes(32, "little") + + +def verify(pk: bytes, msg: bytes, sig: bytes) -> bool: + """验签:通过返回 True;任何不符(含畸形输入)返回 False,不抛异常。""" + if len(pk) != 32 or len(sig) != 64: + return False + R, s = sig[:32], int.from_bytes(sig[32:], "little") + if s >= _Q: + return False + try: + A = _decode_point(pk) + Rpt = _decode_point(R) + except ValueError: + return False + h = _hint(R + pk + msg) % _Q + # [S]B == R + [h]A + return _encode_point(_scalarmult(_B, s)) == _encode_point( + _point_add(Rpt, _scalarmult(A, h))) + + +def fingerprint(pk: bytes) -> str: + """公钥指纹:SHA256(公钥) 十六进制,用于注释与审计对照。""" + return hashlib.sha256(pk).hexdigest() diff --git a/scripts/render/html.py b/scripts/render/html.py index 16ce90e..474a6db 100644 --- a/scripts/render/html.py +++ b/scripts/render/html.py @@ -13,6 +13,7 @@ logo 与品牌字体(render/assets/)以 base64 内嵌,保证 HTML 单文 import base64 import re from datetime import datetime +from html import escape as _escape from pathlib import Path from chart.ganzhi import NAYIN, WX_GAN, WX_ZHI, gz_void, ten_god, terrain @@ -32,6 +33,14 @@ _SEASON_LABEL = ("旺", "相", "休", "囚", "死") _ASSETS_DIR = Path(__file__).resolve().parent / "assets" _SHA_NOTE_RE = re.compile(r"[((][^))]*[))]") +# 结构性占位符:值本身是装配层渲染出的 HTML 片段或 base64 资源, +# 填充时原样注入;其余占位符均为文本字段,一律 HTML 转义防注入。 +_STRUCTURAL_KEYS = frozenset({ + "LOGO_B64", "FONT_B64", "QRCODE_B64", + "WARNINGS", "MAIN_TABLE", "LUCK_STRIP", "CHILDHOOD_BLOCK", + "FLOW_YEARS", "FLOW_MONTHS", +}) + def _strip_sha_note(entry: str) -> str: """神煞条目剔除查法括号注释:`将星(以年支子查,见于月支子)` → `将星`。""" @@ -239,5 +248,8 @@ def build_html(chart) -> str: } out = tpl for k, v in mapping.items(): - out = out.replace("{{" + k + "}}", str(v)) + v = str(v) + if k not in _STRUCTURAL_KEYS: + v = _escape(v, quote=True) + out = out.replace("{{" + k + "}}", v) return out diff --git a/scripts/render/templates/paipan.html b/scripts/render/templates/paipan.html index 545d882..39fba23 100644 --- a/scripts/render/templates/paipan.html +++ b/scripts/render/templates/paipan.html @@ -3,6 +3,7 @@ + {{TITLE}}