codesync.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. # -*- coding: utf-8 -*-
  2. """
  3. 阈值「写回代码」
  4. 需求:在【指标配置】面板改数字,就是改规则本身 —— 不再有「代码默认值」和「库里自定义值」
  5. 两套数字打架。
  6. 做法:
  7. 1. 面板保存 → 先写库(立即生效)
  8. 2. 再把新值写回 rules.py 的 DEFAULT_THRESHOLDS(代码里的「标准」跟着变)
  9. 3. 重新加载 rules 模块
  10. 出厂值单独留一份快照(data/factory_thresholds.json),
  11. 【恢复默认】= 回到出厂值,并且同样写回代码,保证任何时刻库和代码都一致。
  12. """
  13. import importlib
  14. import importlib.util
  15. import json
  16. import os
  17. import re
  18. BASE = os.path.dirname(os.path.abspath(__file__))
  19. RULES_PY = os.path.join(BASE, "rules.py")
  20. DATA_DIR = os.path.join(os.path.dirname(BASE), "data")
  21. FACTORY_FILE = os.path.join(DATA_DIR, "factory_thresholds.json")
  22. def _fmt(v):
  23. """把值格式化成 Python 源码里的写法"""
  24. if isinstance(v, (list, tuple)):
  25. return "[" + ", ".join(_fmt(x) for x in v) + "]"
  26. if isinstance(v, bool):
  27. return "True" if v else "False"
  28. if isinstance(v, float):
  29. if abs(v - round(v)) < 1e-12:
  30. return str(int(round(v)))
  31. return "%.10g" % v # 去掉浮点噪音:0.0028000000000000004 → 0.0028
  32. if isinstance(v, int):
  33. return str(v)
  34. return repr(v)
  35. def _value_re(v):
  36. """匹配某个值在源码里的形式(数字/列表),用于定位替换"""
  37. if isinstance(v, (list, tuple)):
  38. return r"\[[^\[\]]*\]"
  39. return r"[^,\]\}\n#]+"
  40. def write_defaults(T):
  41. """
  42. 把 T 写回 rules.py 的 DEFAULT_THRESHOLDS。
  43. 只改已有键的「值」,保留键名、缩进、行尾注释和其它代码。
  44. 返回 (ok, msg)。
  45. """
  46. try:
  47. with open(RULES_PY, encoding="utf-8") as f:
  48. src = f.read()
  49. except OSError as e:
  50. return False, "读不到 rules.py:%s" % e
  51. start = src.find("DEFAULT_THRESHOLDS = {")
  52. if start < 0:
  53. return False, "rules.py 里找不到 DEFAULT_THRESHOLDS"
  54. end = src.find("\n}", start)
  55. if end < 0:
  56. return False, "DEFAULT_THRESHOLDS 结构异常"
  57. head, block, tail = src[:start], src[start:end], src[end:]
  58. changed = 0
  59. for k, v in T.items():
  60. # 组1=键与冒号 组2=旧值 组3=逗号 + 行尾注释 + 换行
  61. pat = re.compile(
  62. r'("' + re.escape(k) + r'"\s*:\s*)(' + _value_re(v) + r')(\s*,[^\n]*\n)')
  63. m = pat.search(block)
  64. if not m:
  65. continue
  66. if m.group(2).strip() == _fmt(v):
  67. continue
  68. block = block[:m.start()] + m.group(1) + _fmt(v) + m.group(3) + block[m.end():]
  69. changed += 1
  70. if not changed:
  71. return True, "代码默认值已是最新,无需变更"
  72. try:
  73. with open(RULES_PY + ".bak", "w", encoding="utf-8") as f:
  74. f.write(src)
  75. tmp = RULES_PY + ".tmp"
  76. with open(tmp, "w", encoding="utf-8") as f:
  77. f.write(head + block + tail)
  78. os.replace(tmp, RULES_PY)
  79. except OSError as e:
  80. return False, "写 rules.py 失败:%s" % e
  81. # 关键:macOS 自带 Python 把 .pyc 放在 ~/Library/Caches/com.apple.python,
  82. # 不在这里清掉的话,同尺寸同秒的改动会被旧字节码顶掉(踩过)
  83. try:
  84. cache = importlib.util.cache_from_source(RULES_PY)
  85. if os.path.exists(cache):
  86. os.remove(cache)
  87. except Exception:
  88. pass
  89. try:
  90. import rules
  91. importlib.reload(rules)
  92. except Exception as e:
  93. return False, "已写入但重新加载失败:%s(重启服务后生效)" % e
  94. return True, "已同步写回代码(%d 项)" % changed
  95. def get_factory():
  96. """出厂阈值:首次运行时从代码快照一份,之后固定不变"""
  97. os.makedirs(DATA_DIR, exist_ok=True)
  98. if os.path.isfile(FACTORY_FILE):
  99. try:
  100. with open(FACTORY_FILE, encoding="utf-8") as f:
  101. return json.load(f)
  102. except Exception:
  103. pass
  104. import rules
  105. snap = dict(rules.DEFAULT_THRESHOLDS)
  106. try:
  107. with open(FACTORY_FILE, "w", encoding="utf-8") as f:
  108. json.dump(snap, f, ensure_ascii=False, indent=1)
  109. except OSError:
  110. pass
  111. return snap