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
183 lines
5.8 KiB
Python
Executable File
183 lines
5.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""从 student.json 构建 API 请求体,将考生倾向映射到选填参数。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
# 意向城市 → API provinces(省份意向)
|
|
CITY_TO_PROVINCE: dict[str, str] = {
|
|
"北京": "北京",
|
|
"上海": "上海",
|
|
"天津": "天津",
|
|
"重庆": "重庆",
|
|
"深圳": "广东",
|
|
"广州": "广东",
|
|
"东莞": "广东",
|
|
"佛山": "广东",
|
|
"武汉": "湖北",
|
|
"成都": "四川",
|
|
"杭州": "浙江",
|
|
"宁波": "浙江",
|
|
"南京": "江苏",
|
|
"苏州": "江苏",
|
|
"无锡": "江苏",
|
|
"西安": "陕西",
|
|
"长沙": "湖南",
|
|
"郑州": "河南",
|
|
"济南": "山东",
|
|
"青岛": "山东",
|
|
"合肥": "安徽",
|
|
"厦门": "福建",
|
|
"福州": "福建",
|
|
"大连": "辽宁",
|
|
"沈阳": "辽宁",
|
|
"哈尔滨": "黑龙江",
|
|
"长春": "吉林",
|
|
"昆明": "云南",
|
|
"南宁": "广西",
|
|
"海口": "海南",
|
|
"石家庄": "河北",
|
|
"太原": "山西",
|
|
"南昌": "江西",
|
|
"兰州": "甘肃",
|
|
"贵阳": "贵州",
|
|
"呼和浩特": "内蒙古",
|
|
"乌鲁木齐": "新疆",
|
|
"银川": "宁夏",
|
|
"西宁": "青海",
|
|
"拉萨": "西藏",
|
|
}
|
|
|
|
DEFAULT_API_OPTIONS: dict[str, Any] = {
|
|
"universityNum": 30,
|
|
"majorNum": 6,
|
|
"isAdjust": True,
|
|
"volunteerType": "ACADEMY_GROUP",
|
|
"returnUniversityNum": 30,
|
|
"intentionNum": 0,
|
|
}
|
|
|
|
|
|
def _join_csv(values: list[str] | None) -> str | None:
|
|
if not values:
|
|
return None
|
|
cleaned = [str(v).strip() for v in values if str(v).strip()]
|
|
return ",".join(cleaned) if cleaned else None
|
|
|
|
|
|
def cities_to_provinces(cities: list[str] | None) -> list[str]:
|
|
"""意向城市去重映射为省份列表。"""
|
|
if not cities:
|
|
return []
|
|
provinces: list[str] = []
|
|
seen: set[str] = set()
|
|
for city in cities:
|
|
key = str(city).strip().replace("市", "").replace("省", "")
|
|
if not key:
|
|
continue
|
|
province = CITY_TO_PROVINCE.get(key, key)
|
|
if province not in seen:
|
|
seen.add(province)
|
|
provinces.append(province)
|
|
return provinces
|
|
|
|
|
|
def student_to_api_payload(student: dict[str, Any]) -> dict[str, Any]:
|
|
"""将 student.json 转为 API 请求体。"""
|
|
preferred_provinces = student.get("preferred_provinces") or []
|
|
if not preferred_provinces:
|
|
preferred_provinces = cities_to_provinces(student.get("preferred_cities"))
|
|
|
|
payload: dict[str, Any] = {
|
|
"province": student.get("province"),
|
|
"classify": student.get("classify"),
|
|
"subjects": student.get("subjects"),
|
|
"gradeType": student.get("gradeType"),
|
|
"score": student.get("score"),
|
|
"rank": student.get("rank"),
|
|
"batch": student.get("batch"),
|
|
"universitys": _join_csv(student.get("preferred_universities")),
|
|
"provinces": _join_csv(preferred_provinces),
|
|
"tags": _join_csv(student.get("preferred_tags")),
|
|
"majorClass": _join_csv(student.get("preferred_major_classes")),
|
|
**DEFAULT_API_OPTIONS,
|
|
}
|
|
|
|
overrides = student.get("api_overrides") or {}
|
|
for key, value in overrides.items():
|
|
if value is not None:
|
|
payload[key] = value
|
|
|
|
return payload
|
|
|
|
|
|
def summarize_preferences(student: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]:
|
|
"""返回倾向映射摘要,便于 Agent 向用户说明。"""
|
|
return {
|
|
"universitys": payload.get("universitys"),
|
|
"provinces": payload.get("provinces"),
|
|
"tags": payload.get("tags"),
|
|
"majorClass": payload.get("majorClass"),
|
|
"sources": {
|
|
"preferred_universities": student.get("preferred_universities"),
|
|
"preferred_cities": student.get("preferred_cities"),
|
|
"preferred_provinces": student.get("preferred_provinces"),
|
|
"preferred_tags": student.get("preferred_tags"),
|
|
"preferred_major_classes": student.get("preferred_major_classes"),
|
|
},
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="从 student.json 构建 API 请求 JSON")
|
|
parser.add_argument("-i", "--input", type=Path, required=True, help="student.json 路径")
|
|
parser.add_argument("-o", "--output", type=Path, required=True, help="输出 api_request.json")
|
|
parser.add_argument(
|
|
"--summary",
|
|
type=Path,
|
|
help="可选:输出倾向映射摘要 JSON(供 Agent 核对)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
student = json.loads(args.input.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError) as e:
|
|
print(f"错误: 无法读取 student.json — {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
payload = student_to_api_payload(student)
|
|
try:
|
|
from province_config import validate_classify, validate_subjects_for_volunteer_api
|
|
|
|
validate_classify(str(payload.get("province", "")), payload.get("classify"))
|
|
validate_subjects_for_volunteer_api(payload)
|
|
except ValueError as e:
|
|
print(f"错误: {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
required = ["province", "classify", "score"]
|
|
missing = [k for k in required if not payload.get(k)]
|
|
if missing:
|
|
print(f"错误: student.json 缺少 API 必填字段 {missing}", file=sys.stderr)
|
|
return 1
|
|
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
print(f"已生成 API 请求: {args.output}")
|
|
|
|
if args.summary:
|
|
summary = summarize_preferences(student, payload)
|
|
args.summary.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
print(f"已生成倾向映射摘要: {args.summary}")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|