redact.py 3.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. # -*- coding: utf-8 -*-
  2. """公开模式脱敏: 隐藏核心参数/经验值/阈值 (用户 2026-08-23 令). 默认公开; 内部分析设 WINDCMS_INTERNAL=1 才显示全部.
  3. 原则: 数据 (实测值/倍数/台号) 可见, 判据的数值门槛不可见; 判据名 (G1..G6/绝对锚/污染门) 可见, 其阈值替换为 ▪."""
  4. import os, re
  5. PUBLIC = os.environ.get('WINDCMS_INTERNAL') != '1'
  6. # 阈值模式: 比较符后面的数字 (含单位), p90 倍数, CV 界, bin 容差, 门槛词后数字
  7. _PATS = [
  8. (re.compile(r'(≥|>=|≤|<=|>|<|>|<)\s*=?\s*[0-9][0-9.,]*\s*(×|%|µs|us|ms|bin|dB|Hz|kHz|窗|条|台|倍|σ)?'), r'\1▪'),
  9. (re.compile(r'(阈值?|阈|门槛|门限|判据|容差|界)\s*[:=:]?\s*[0-9][0-9.]*\s*(×|%|µs|bin|Hz|倍)?'), r'\1▪'),
  10. (re.compile(r'[0-9][0-9.]*\s*×\s*(同\s*shard\s*)?(fleet\s*)?p90'), '▪×p90'),
  11. (re.compile(r'p90\s*[0-9][0-9.]*'), 'p90 ▪'),
  12. (re.compile(r'CV\s*(≤|<=|<)\s*[0-9][0-9.]*%?'), 'CV≤▪'),
  13. (re.compile(r'(相干|coh\w*|群延迟|gd|相位合成向量 R|R|峰/本底|峰/邻域|选择性|结构度|个体|持续|占比|share|weak_over_p90|min_over_p90|min_share)\s*(≥|>=|≤|<=|>|<|=)\s*[0-9][0-9.]*\s*(×|%|µs|bin|窗)?', re.I), r'\1\2▪'),
  14. (re.compile(r'(coh_shared|gd_max_us|r_min|coh_independent|min_share|min_over_p90|weak_over_p90)\s*=\s*[0-9][0-9.]*'), r'\1=▪'),
  15. (re.compile(r'±\s*[0-9][0-9.]*\s*(bin|Hz|%)'), '±▪\\1'),
  16. (re.compile(r'(\d+(?:\.\d+)?)\s*(bin)\b'), '▪ \\2'),
  17. # 特征频率一律隐藏 (用户 2026-08-23): 任何 数值+Hz/kHz/阶, 频带 a–b kHz, @xxxHz, 齿数 (LSS×92 类)
  18. (re.compile(r'\d+(?:\.\d+)?\s*[–\-~]\s*\d+(?:\.\d+)?\s*(k?Hz)'), '▪–▪ \\1'),
  19. (re.compile(r'@\s*\d+(?:\.\d+)?\s*(k?Hz)'), '@▪\\1'),
  20. (re.compile(r'\d+(?:\.\d+)?\s*(k?Hz|赫兹)'), '▪ \\1'),
  21. (re.compile(r'(\d+(?:\.\d+)?)\s*阶'), '▪阶'),
  22. (re.compile(r'(LSS|C2|HSS|IMS|转频|环齿|小齿轮齿)\s*×\s*\d+'), '\\1×▪'),
  23. (re.compile(r'(BPFI|BPFO|BSF|FTF|GMF\d?|1P|2P|3P)\s*[==]\s*\d+(?:\.\d+)?'), '\\1=▪'),
  24. ]
  25. _HZ_KEYS = ('hz', 'peak_hz', 'freq', 'frequency', 'f_e_hz', 'fgen2_hz', 'comb_spacing_med_hz', 'line_hz')
  26. _PHRASES = [
  27. ('峰/邻域≥3 才算有线', '峰/邻域达到判据才算有线'), ('G2 有线阈 3.0', 'G2 有线判据'), ('(峰/邻域≥3 才算有线, 定级仍须六闸+绝对锚)', '(判线走六道闸与绝对锚)'),
  28. ]
  29. def redact(text):
  30. if not PUBLIC or text is None:
  31. return text
  32. s = str(text)
  33. for a, b in _PHRASES:
  34. s = s.replace(a, b)
  35. for pat, rep in _PATS:
  36. s = pat.sub(rep, s)
  37. return s
  38. def redact_obj(o):
  39. """递归脱敏 dict/list 里的字符串 (插件返回的 data/text/reason)."""
  40. if not PUBLIC:
  41. return o
  42. if isinstance(o, str):
  43. return redact(o)
  44. if isinstance(o, dict):
  45. return {k: ('▪' if str(k).lower() in _HZ_KEYS else redact_obj(v)) for k, v in o.items()}
  46. if isinstance(o, list):
  47. return [redact_obj(v) for v in o]
  48. return o
  49. def redact_always(text):
  50. """**强制**脱敏, 不看 PUBLIC 开关。
  51. 用于**客户版交付物** — 客户版的脱敏不能依赖 WINDCMS_INTERNAL 环境变量:
  52. 谁在内部模式下跑一次 build, 客户版 HTML 就会连特征频率一起写进去 (2026-08-28 自逮)。
  53. 交付物的脱敏必须是**结构性的**, 不是配置性的。
  54. """
  55. if text is None:
  56. return text
  57. s = str(text)
  58. for a, b in _PHRASES:
  59. s = s.replace(a, b)
  60. for pat, rep in _PATS:
  61. s = pat.sub(rep, s)
  62. return s