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