Files
Z User e45b68bb8b 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
2026-10-27 22:50:50 +00:00

167 lines
6.0 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())