# -*- coding: utf-8 -*- """为 Bruno collection docs 块内的裸 JSON 补 ```json fence。 规则(SKILL.md「示例生成」):补 fence 属格式修复,示例值逐字保留;只插入 fence 行,不改任何既有行。 用法:python fix_bru_json.py [--write] 不带 --write 为 dry-run。 """ import io, os, sys ROOT = r"D:/code/crm-api-docs" WRITE = "--write" in sys.argv sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") def process(path): """返回 (n_blocks, n_skipped_fenced, ok)""" with io.open(path, "r", encoding="utf-8-sig", newline="") as f: raw = f.read() eol = "\r\n" if "\r\n" in raw else "\n" lines = raw.splitlines(keepends=True) # 定位 docs 块 try: docs_start = next(i for i, l in enumerate(lines) if l.strip() == "docs {") except StopIteration: return (0, 0, None) # 无 docs 块 # 检查 generated 标记(meta 块内) generated = any("generated" in l for l in lines[:docs_start]) if not generated: return (0, 0, "private") # 找裸 JSON 块(在 docs 块内) out, i, n_bare, n_fenced, anomaly = [], 0, 0, 0, False n = len(lines) while i < n: line = lines[i] if line.strip() == "{" and i >= docs_start: # 前一行已 fence 则跳过 if out and out[-1].strip() == "```json": n_fenced += 1 out.append(line) i += 1 continue # 追踪 depth 找块尾 depth, j = 0, i while j < n: t = lines[j] depth += t.count("{") - t.count("}") if depth <= 0: break j += 1 if j >= n or depth != 0: anomaly = True break indent = line[: len(line) - len(line.lstrip())] out.append(indent + "```json" + eol) out.extend(lines[i : j + 1]) out.append(indent + "```" + eol) n_bare += 1 i = j + 1 continue out.append(line) i += 1 if anomaly: return (0, 0, "anomaly") if n_bare and WRITE: with io.open(path, "w", encoding="utf-8", newline="") as f: f.write("".join(out)) return (n_bare, n_fenced, "ok") stats = {"files": 0, "blocks": 0, "fenced": 0, "private": [], "anomaly": [], "nodocs": 0} for dirpath, dirnames, filenames in os.walk(ROOT): dirnames[:] = [d for d in dirnames if d not in ("environments", ".git")] for fn in sorted(filenames): if not fn.endswith(".bru") or fn == "collection.bru": continue path = os.path.join(dirpath, fn) rel = path.replace(ROOT + os.sep, "").replace(ROOT + "/", "") nb, nf, status = process(path) if status == "private": stats["private"].append(rel) continue if status == "anomaly": stats["anomaly"].append(rel) print("ANOMALY-SKIPPED\t%s" % rel) continue if status is None: stats["nodocs"] += 1 continue if nb: stats["files"] += 1 stats["blocks"] += nb print("%s\t%s\t+%d fence" % ("WRITE" if WRITE else "PLAN", rel, nb)) stats["fenced"] += nf print("---- %s: %d files, %d bare blocks fenced, %d already fenced" % ( "WRITTEN" if WRITE else "DRY-RUN", stats["files"], stats["blocks"], stats["fenced"])) if stats["private"]: print("private skipped (%d): %s" % (len(stats["private"]), ", ".join(stats["private"]))) if stats["anomaly"]: print("ANOMALY (%d): %s" % (len(stats["anomaly"]), ", ".join(stats["anomaly"])))