You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

210 lines
9.2 KiB

# -*- 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 写回完成 ==')