feat: IMAP mail sync with thread-to-contact matching (PRD §6.1)
Real IMAP connection via imapflow library: - Connects to IMAP server (IMAPFlow, TLS on port 993) - Fetches recent messages (configurable limit, default 20) - Extracts: from, fromEmail, subject, date, snippet (200 chars) - Thread-to-contact matching (PRD §6.1): 1. Match sender email against persons table 2. If no person match, extract domain and match against company.website 3. Returns matchedCompany + matchedPerson for each email - Audit-logged (action: crm.mailsync:imap) - Zod validated (host, port, user, password, folder, limit) - 502 on IMAP connection failure with detail - 400 on validation failure Tested: invalid creds → 502 'IMAP connection failed', missing host → 400
This commit is contained in:
1 parent
7acc0c5451
commit
e45b68bb8b
1089 files changed
+260614
No files matched your search
+166
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""批量测试 batch/list 接口:各省 × 正确/错误 classify × 高/低分。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
# 允许从 scripts/ 目录直接运行
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from province_config import ( # noqa: E402
|
||||
MODE_312,
|
||||
MODE_33,
|
||||
MODE_OLD,
|
||||
UNSUPPORTED_PROVINCES,
|
||||
batch_list_query_params,
|
||||
infer_grade_type,
|
||||
)
|
||||
|
||||
BASE_URL = "https://publicapi.chatglm.cn/chatglm_public/skill-test"
|
||||
BATCH_LIST_PATH = "/zp/volunteer/batch/list"
|
||||
|
||||
PROVINCE_CLASSIFY: dict[str, list[str]] = {}
|
||||
for p in MODE_33:
|
||||
PROVINCE_CLASSIFY[p] = ["综合"]
|
||||
for p in MODE_312:
|
||||
PROVINCE_CLASSIFY[p] = ["物理", "历史"]
|
||||
for p in MODE_OLD:
|
||||
PROVINCE_CLASSIFY[p] = ["文科", "理科"]
|
||||
for p in UNSUPPORTED_PROVINCES:
|
||||
PROVINCE_CLASSIFY[p] = ["综合"]
|
||||
|
||||
WRONG_CLASSIFY = {
|
||||
"综合": "物理",
|
||||
"物理": "综合",
|
||||
"历史": "理科",
|
||||
"文科": "物理",
|
||||
"理科": "历史",
|
||||
}
|
||||
|
||||
HIGH_SCORE = 650
|
||||
LOW_SCORE = 280
|
||||
|
||||
|
||||
def call_batch_from_payload(payload: dict) -> dict:
|
||||
params = batch_list_query_params(dict(payload))
|
||||
query = urlencode(params)
|
||||
url = f"{BASE_URL.rstrip('/')}{BATCH_LIST_PATH}?{query}"
|
||||
req = Request(url, method="GET", headers={"Accept": "*/*"})
|
||||
try:
|
||||
with urlopen(req, timeout=30) as resp:
|
||||
text = resp.read().decode(resp.headers.get_content_charset() or "utf-8")
|
||||
except HTTPError as e:
|
||||
return {"http": e.code, "body": e.read().decode("utf-8", errors="replace")}
|
||||
except URLError as e:
|
||||
return {"error": str(e.reason)}
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return {"error": "invalid json", "body": text[:200]}
|
||||
|
||||
|
||||
def summarize_result(raw: dict) -> str:
|
||||
if "error" in raw or "http" in raw:
|
||||
return f"FAIL:{raw.get('http') or raw.get('error')}"
|
||||
status = raw.get("status")
|
||||
msg = raw.get("message", "")
|
||||
result = raw.get("result")
|
||||
if status != 0:
|
||||
return f"FAIL:status={status},{msg}"
|
||||
if not isinstance(result, list):
|
||||
return "FAIL:result not list"
|
||||
if not result:
|
||||
return "OK:empty[]"
|
||||
batches = [f"{x.get('batch')}({x.get('gradeType')}/{x.get('type')})" for x in result[:3]]
|
||||
extra = f"+{len(result)-3}" if len(result) > 3 else ""
|
||||
return f"OK:{len(result)}项 [{', '.join(batches)}{extra}]"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
rows: list[dict] = []
|
||||
provinces = sorted(PROVINCE_CLASSIFY.keys(), key=lambda x: (x not in UNSUPPORTED_PROVINCES, x))
|
||||
|
||||
print("=== 正确 classify × 高分/低分(京沪津含 gradeType)===")
|
||||
for province in provinces:
|
||||
classifies = PROVINCE_CLASSIFY[province]
|
||||
primary = classifies[0]
|
||||
for score_label, score in [("高", HIGH_SCORE), ("低", LOW_SCORE)]:
|
||||
payload = {
|
||||
"province": province,
|
||||
"classify": primary,
|
||||
"score": score,
|
||||
"batch": "本科批",
|
||||
}
|
||||
if province in ("北京", "上海", "天津"):
|
||||
payload["gradeType"] = infer_grade_type(province, score, batch_pref="本科批")
|
||||
try:
|
||||
raw = call_batch_from_payload(payload)
|
||||
except ValueError as e:
|
||||
raw = {"status": -1, "message": str(e)}
|
||||
summary = summarize_result(raw)
|
||||
rows.append({
|
||||
"province": province,
|
||||
"classify": primary,
|
||||
"score": score,
|
||||
"score_label": score_label,
|
||||
"gradeType": payload.get("gradeType"),
|
||||
"wrong_classify": False,
|
||||
"summary": summary,
|
||||
"status": raw.get("status"),
|
||||
"message": raw.get("message"),
|
||||
"count": len(raw.get("result") or []) if isinstance(raw.get("result"), list) else None,
|
||||
})
|
||||
mark = "✓" if summary.startswith("OK") and "empty" not in summary else "✗"
|
||||
gt = payload.get("gradeType") or ""
|
||||
print(f" {mark} {province:4s} {primary} {gt:2s} {score_label}分({score}) → {summary}")
|
||||
time.sleep(0.12)
|
||||
|
||||
print("\n=== 错误 classify 探测(高分)===")
|
||||
for province in provinces:
|
||||
if province in UNSUPPORTED_PROVINCES:
|
||||
continue
|
||||
primary = PROVINCE_CLASSIFY[province][0]
|
||||
wrong = WRONG_CLASSIFY.get(primary, "综合" if primary != "综合" else "物理")
|
||||
ok_payload = {"province": province, "classify": primary, "score": HIGH_SCORE, "batch": "本科批"}
|
||||
wrong_payload = dict(ok_payload, classify=wrong)
|
||||
if province in ("北京", "上海", "天津"):
|
||||
ok_payload["gradeType"] = infer_grade_type(province, HIGH_SCORE, batch_pref="本科批")
|
||||
wrong_payload["gradeType"] = ok_payload["gradeType"]
|
||||
try:
|
||||
raw_ok = call_batch_from_payload(ok_payload)
|
||||
raw_wrong = call_batch_from_payload(wrong_payload)
|
||||
except ValueError as e:
|
||||
print(f" ! {province}: validate error {e}")
|
||||
continue
|
||||
ok_s = summarize_result(raw_ok)
|
||||
wrong_s = summarize_result(raw_wrong)
|
||||
if wrong_s != ok_s:
|
||||
print(f" ! {province}: 正确({primary})={ok_s} vs 错误({wrong})={wrong_s}")
|
||||
time.sleep(0.12)
|
||||
|
||||
out = Path(__file__).resolve().parent.parent / "output" / "batch_api_test_results.json"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"\n详细结果已保存: {out}")
|
||||
|
||||
fails = [
|
||||
r for r in rows
|
||||
if not r["summary"].startswith("OK") or "empty[]" in r["summary"]
|
||||
if not r.get("wrong_classify")
|
||||
]
|
||||
print(f"\n=== 汇总: 正确参数异常 {len(fails)} 项 ===")
|
||||
for r in fails:
|
||||
print(f" {r['province']} {r['classify']} {r['score_label']}分 → {r['summary']} | {r.get('message')}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user