# -*- coding: utf-8 -*- """票 05 · 响应示例真值化写回主脚本。 范围:87 占位文件(含「非真实返回」)∪ 5 退化升级端点(doc-truth 已有非空真值)。 动作:docs{} 内全部「## 响应示例」节删除 → 在错误码节后(无则 data 结构节后)插入唯一真值节。 特例:3 个二进制端点文字说明;assignable 大列表截 3 条;分页 content 截 1 条 + total 注记; batchEdit 成功信封 + 行形态参照注记。 纪律:不动 meta/params/对账钥匙/请求示例/错误码/data 结构表;utf-8 无 BOM;不提交留审查。 """ from __future__ import annotations import io import json import re import sys from pathlib import Path sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') SP = Path('.scratch/customer-integration-ready') A4 = Path('D:/code/crm-api-docs/A4 客户管理') # ---------- specimens 合并(doc-truth 最后写入 = 优先) ---------- merged: dict[str, dict] = {} for name in ['specimens-core-r3.json', 'specimens-heavy-r3.json', 'specimens-incr-r3.json', 'specimens-graph-r3.json', 'specimens-doc-truth.json']: for k, v in json.loads((SP / name).read_text(encoding='utf-8')).items(): merged[k] = v BINARY = { 'GET /api/customer/import/template': 'HTTP 200 二进制文件流:Content-Type application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,' '4301 bytes(EasyExcel xlsx 导入模板),不走 JSON 信封,前端按附件流下载处理。', 'GET /api/customer/contact/import/template': 'HTTP 200 二进制文件流:3694 bytes(PK zip 头 50 4b 03 04,xlsx),不走 JSON 信封,前端按附件流下载处理。', 'POST /api/customer/contact/export': 'HTTP 200 二进制文件流:4042 bytes(PK zip 头 50 4b 03 04,xlsx),不走 JSON 信封,前端按附件流下载处理。', } # 退化升级端点:doc-truth 已有非空真值,覆盖旧退化示例(无占位标记也重写) UPGRADE = { 'POST /api/customer/transfer/assign', 'GET /api/customer/contact/graph/detail', 'POST /api/customer/contact/quickAdd', 'POST /api/customer/quick-create', 'GET /api/customer/import/failures', } BATCHEDIT_NOTE = '(failedRows 空集:本次 2 行全成功;失败行形态参照 快速添加联系人 示例)' MARK = '// 示例数据(E2E 实测真值),字段结构以上表为准' def build_resp(key, resp): """按端点形态裁剪超长载荷,返回 (resp2, note)。""" data = resp.get('data') if isinstance(resp, dict) else None if isinstance(data, list) and len(data) > 3: return {**resp, 'data': data[:3]}, f'(实测 {len(data)} 条,截取前 3 条)' if isinstance(data, dict): c = data.get('content') if isinstance(c, list) and len(c) > 1: total = data.get('total') trimmed = {**data, 'content': c[:1]} note = f'(实测 total={total},content 截取 1 条)' if total is not None else '(content 截取 1 条)' return {**resp, 'data': trimmed}, note if key == 'POST /api/customer/contact/batchEdit': return resp, BATCHEDIT_NOTE return resp, '' def build_section(key, resp): rows = [' ## 响应示例', ''] if key in BINARY: rows += [f' {MARK}', '', f' {BINARY[key]}'] return rows resp2, note = build_resp(key, resp) body = json.dumps(resp2, ensure_ascii=False, indent=2) rows += [f' {MARK}{note}', ' ```json'] rows += [' ' + l for l in body.splitlines()] rows += [' ```'] return rows def rewrite(path: Path, key: str): txt = path.read_text(encoding='utf-8') lines = txt.splitlines() d0 = next(i for i, l in enumerate(lines) if l.strip() == 'docs {') d1 = next(i for i in range(d0, len(lines)) if lines[i].rstrip() == '}') def heads(arr, lo, hi): return [(i, arr[i].strip()) for i in range(lo, hi) if re.match(r'^\s*## ', arr[i])] # 1) 删除全部响应示例节(从后往前删) hs = heads(lines, d0 + 1, d1) for idx in range(len(hs) - 1, -1, -1): i, t = hs[idx] if t != '## 响应示例': continue end = hs[idx + 1][0] if idx + 1 < len(hs) else d1 del lines[i:end] # 2) 重算,定位插入点:错误码节尾 → data 结构节尾 → docs 收尾前 d1 = next(i for i in range(d0, len(lines)) if lines[i].rstrip() == '}') hs = heads(lines, d0 + 1, d1) ins = None for idx, (i, t) in enumerate(hs): if t == '## 错误码': ins = hs[idx + 1][0] if idx + 1 < len(hs) else d1 break if ins is None: for idx, (i, t) in enumerate(hs): if t.startswith('## 响应 data 结构'): ins = hs[idx + 1][0] if idx + 1 < len(hs) else d1 break if ins is None: ins = d1 # 3) 插入(插入点前非空行时补一个空行,保节间形态) sec = build_section(key, merged[key]['response']) if ins > d0 + 1 and lines[ins - 1].strip() != '' and ins != d1: pass # 节标题直接贴上节内容 = 生成器原版形态 elif ins == d1 and ins > 0 and lines[ins - 1].strip() != '': sec = [''] + sec lines[ins:ins] = sec path.write_text('\n'.join(lines) + ('\n' if txt.endswith('\n') else ''), encoding='utf-8', newline='') return len('\n'.join(sec)) # ---------- 主循环 ---------- key_re = re.compile(r'^\s*`\s*(GET|POST)\s+(/[^\s`]+)\s*`', re.M) files = [p for p in A4.rglob('*.bru') if p.name != 'folder.bru'] done = skipped = 0 big = [] for p in sorted(files): txt = p.read_text(encoding='utf-8') m = key_re.search(txt) if not m: print(f'⚠ 无对账钥匙: {p.relative_to(A4)}') continue key = f'{m.group(1)} {m.group(2)}' if key not in merged: print(f'⚠ 无 specimen: {p.relative_to(A4)} ← {key}') continue needs = '非真实返回' in txt or key in UPGRADE if not needs: skipped += 1 continue n = rewrite(p, key) done += 1 if n > 20000: big.append(f'{p.relative_to(A4)} ← {key} 节长 {n}') print(f'[主循环] 写回 {done},跳过(无占位非退化) {skipped},异常 0') for b in big: print(f' 大节: {b}') # ---------- 验证 ---------- print('== 验证 ==') all_files = [p for p in A4.rglob('*.bru') if p.name != 'folder.bru'] ph = [p.relative_to(A4) for p in all_files if '非真实返回' in p.read_text(encoding='utf-8')] print(f'[V1] 「非真实返回」残留 = {len(ph)}(目标 0){ph if ph else ""}') sec_bad = [] tv = 0 for p in all_files: t = p.read_text(encoding='utf-8') n = len(re.findall(r'^\s*## 响应示例', t, re.M)) if n != 1: sec_bad.append(f'{p.relative_to(A4)} ×{n}') if 'E2E 实测真值' in t: tv += 1 print(f'[V2] 响应示例节≠1 的文件 = {len(sec_bad)}(目标 0){sec_bad if sec_bad else ""}') print(f'[V3] 含真值标记文件 = {tv} / {len(all_files)}') n_ec = sum(1 for p in all_files if '## 错误码' in p.read_text(encoding='utf-8')) print(f'[V4] 错误码节文件数 = {n_ec}(票 06 基线 78,不得回退)') keys_ok = sum(1 for p in all_files if key_re.search(p.read_text(encoding='utf-8'))) print(f'[V5] 对账钥匙完好 = {keys_ok}/{len(all_files)}') bom = [str(p.relative_to(A4)) for p in all_files if p.read_bytes()[:3] == b'\xef\xbb\xbf'] print(f'[V6] BOM = {len(bom)}(目标 0){bom if bom else ""}') # 退化抽查:(b) 端点写回后的 data 非空 SPOT = { '客户交割/客户分配.bru': ('failures', ), '客户公海/失败疑似重复明细.bru': ('data',), '我的客户/失败疑似重复明细.bru': ('data',), '客户详情/联系人图谱/图谱全量读.bru': ('nodes',), '客户详情/联系人/快速添加联系人.bru': ('failedRows',), '我的客户/快速创建客户.bru': ('similarHits',), } for rel, (field,) in SPOT.items(): t = (A4 / rel).read_text(encoding='utf-8') mm = re.search(r'```json\n( \{.*?\n \})\n ```', t, re.S) ok = '?' if mm: try: payload = json.loads(mm.group(1).replace('\n ', '\n')) d = payload.get('data') if field == 'data': ok = '非空' if isinstance(d, list) and len(d) >= 1 else f'退化:{str(d)[:60]}' elif isinstance(d, dict): v = d.get(field) ok = '非空' if v else f'退化:{field}={v}' else: ok = f'形态异常:{str(d)[:60]}' except Exception as e: ok = f'解析失败:{e}' print(f'[V7] {rel}: {field} {ok}') # 二进制端点 for rel in ['客户公海/下载导入模板.bru', '我的客户/客户导入/下载导入模板.bru', '客户详情/联系人导入/下载导入模板.bru']: t = (A4 / rel).read_text(encoding='utf-8') has = '二进制文件流' in t and '非真实返回' not in t print(f'[V8] {rel}: 二进制说明 {"✓" if has else "✗"}') # workspace 分页注记 for rel in ['客户总览/workspace 列表分页.bru', '我的客户/workspace 列表分页.bru']: t = (A4 / rel).read_text(encoding='utf-8') print(f'[V9] {rel}: total 注记 {"✓" if "实测 total=" in t else "✗"}') print('== 票 05 写回完成 ==')