import json, re with open("scripts/openapi.json", encoding="utf-8") as f: spec = json.load(f) real_paths = set() real_path_methods = {} for p, methods in spec.get("paths", {}).items(): real_paths.add(p) for m in methods: real_path_methods.setdefault(p, []).append(m.upper()) with open("src/api/api.ts", encoding="utf-8") as f: lines = f.readlines() func_re = re.compile(r"^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(") static_req = re.compile(r"request(?:<[^>]*>)?\(\s*['\"]([^'\"]+)['\"]") tmpl_req = re.compile(r"request(?:<[^>]*>)?\(\s*`([^`]*)`") dai_re = re.compile(r"【待确认】") cur = None funcs = {} for i, ln in enumerate(lines): m = func_re.match(ln) if m: cur = m.group(1) funcs[cur] = {"static": [], "tmpl": [], "dai": False, "line": i+1} if cur: for sm in static_req.finditer(ln): funcs[cur]["static"].append(sm.group(1)) for tm in tmpl_req.finditer(ln): funcs[cur]["tmpl"].append(tm.group(1)) if dai_re.search(ln): funcs[cur]["dai"] = True # Manual real-equivalent judgment for template/partial/path-wrong functions # value: (verdict, real_path_or_None, note) manual = { # template literals - verify "createJobReview": ("ALIGNED", "POST /jobs/{jobId}/reviews", "路径命中真实契约"), "fetchEmployeeReviews": ("ALIGNED", "GET /jobs/{jobId}/reviews", "路径命中真实契约"), "fetchJobDetail": ("ALIGNED", "GET /jobs/{jobId}", "路径命中真实契约"), "fetchCandidateDetail": ("WRONG->REAL", "/company/candidates/{applicationId}", "前端猜 /enterprise/candidates,真路径在 /company 域"), "fetchInterviewScore": ("WRONG->REAL", "GET /interviews/{interviewId}/score", "前端猜 /enterprise/interviews,真路径在 /interviews 域"), "fetchInterviewScoreDraft": ("WRONG->REAL", "PUT /interviews/{interviewId}/score", "真路径无 /draft 子路径,保存草稿用 PUT /interviews/{id}/score"), "fetchConversation": ("WRONG->REAL", "GET /conversations/{id}", "前端猜 /chat/conversations,真路径 /conversations/{id}"), "deleteSalaryRecord": ("PHANTOM", None, "无 /salary 域,后端未提供"), "fetchLegalDocument": ("PHANTOM", None, "无 /legal 域,后端未提供"), "fetchPrivacyDetailSetting": ("PHANTOM", None, "无 /settings 域,后端未提供"), "fetchSalaryData": ("PHANTOM", None, "无 /jobs/{id}/salary,后端未提供薪资明细端点"), "fetchJobOverallScore": ("PHANTOM", None, "无 /jobs/{id}/score,综合分见 /jobs/{id}/competition-ranking 或 /profile/me/candidate-scoring"), # partial static "fetchJobPreference": ("WRONG->REAL", "GET/PUT /profile/me/preferences", "前端猜 /profile/me/job-preference,真路径 /profile/me/preferences"), "saveJobPreference": ("WRONG->REAL", "PUT /profile/me/preferences", "同上"), "fetchPendingOfferCount": ("PHANTOM", None, "无 /offers/pending/count,可用 /offers/statistics 替代(需确认)"), "fetchRankData": ("WRONG->REAL", "GET /jobs/{jobId}/competition-ranking", "前端猜 /jobs/rank,真为按岗位竞争力排名,语义不同(需 jobId)"), "fetchWeightSettings": ("PHANTOM", None, "无 /jobs/weight-settings,权重见 /profile/me/review-weights、/review-weight-templates"), # already-correct static "fetchJobList": ("ALIGNED", "GET /jobs", "命中真实契约"), "fetchOfferCompareData": ("ALIGNED", "GET /offers/compare", "命中真实契约"), "fetchUserProfile": ("ALIGNED", "GET /profile/me", "命中真实契约"), # path-wrong (phantom base) that have real equivalents elsewhere "fetchConversations": ("WRONG->REAL", "GET /conversations", "前端猜 /messages/conversations,真 /conversations"), "fetchNotifications": ("WRONG->REAL", "GET /notifications", "前端猜 /messages/notifications,真 /notifications"), "fetchInterviews": ("WRONG->REAL", "GET /interviews/me", "前端猜 /messages/interviews,真 /interviews/me(我的面试列表)"), "fetchMatchResult": ("WRONG->REAL", "GET /applications/{id}/match 或 /jobs/{jobId}/match", "前端猜 /messages/match-result,真为投递/岗位匹配诊断"), "fetchResume": ("WRONG->REAL", "GET /profile/me/resume", "前端猜 /resume,真 /profile/me/resume(下载简历)"), "fetchFavorites": ("WRONG->REAL", "GET /jobs/favorites", "前端猜 /profile/me/favorites,真 /jobs/favorites"), "fetchJobProgress": ("WRONG->REAL", "GET /profile/me/career-progress", "前端猜 /tools/job-progress,真 /profile/me/career-progress(统一求职进度聚合)"), "fetchResumeSavedAt": ("PHANTOM", None, "无 /resume/saved-at,简历保存时间可由 /profile/me/resume-versions 派生"), "fetchResumeExportSettings": ("PHANTOM", None, "无 /resume/export-settings,导出设置本地维护"), "saveResumeBasicInfo": ("WRONG->REAL", "POST /profile/me/resume (multipart)", "前端猜 /resume/basic-info,真为上传简历文件(multipart),结构化字段在 PUT /profile/me"), "saveResumeEducation": ("PHANTOM", None, "无 /resume/education,简历整体由 /profile/me/resume + resume-versions 管理"), "saveResumeExperience": ("PHANTOM", None, "同上"), "saveResumeProjects": ("PHANTOM", None, "同上"), "saveResumeSelfEvaluation": ("PHANTOM", None, "同上"), "saveResumeSkills": ("PHANTOM", None, "同上"), "saveResumeSnapshot": ("PHANTOM", None, "同上"), "saveSalaryRecord": ("PHANTOM", None, "无 /salary/records"), "saveWeightSettings": ("PHANTOM", None, "无 /weight-settings"), } def base_has_real(p): segs = [s for s in p.split("/") if s and "{" not in s and "$" not in s] if not segs: return False base = "/" + segs[0] return any(rp == base or rp.startswith(base + "/") for rp in real_paths) verdicts = {} for name, info in funcs.items(): if not info["static"] and not info["tmpl"]: continue if name in manual: v, rp, note = manual[name] verdicts[name] = (v, rp, note, info["dai"], info["line"]) continue # auto paths = info["static"] + info["tmpl"] # for static-only auto allphantom = True anyok = False for p in info["static"]: if p in real_paths: anyok = True; allphantom = False elif base_has_real(p): allphantom = False if anyok: verdicts[name] = ("ALIGNED", ", ".join(info["static"]), "命中真实契约", info["dai"], info["line"]) elif allphantom is False and info["static"]: verdicts[name] = ("PHANTOM", ", ".join(info["static"]), "父域不存在于 OpenAPI", info["dai"], info["line"]) else: verdicts[name] = ("PHANTOM", ", ".join(paths), "未命中 OpenAPI", info["dai"], info["line"]) # also catch functions flagged dai but no request captured (e.g. buildPrivacySummaryText) - skip order = {"ALIGNED":0, "WRONG->REAL":1, "PHANTOM":2} rows = sorted(verdicts.items(), key=lambda kv: (order.get(kv[1][0],9), kv[0])) from collections import Counter c = Counter(v[0] for v in verdicts.values()) print("SUMMARY:", dict(c), "TOTAL FUNCS MAPPED:", len(verdicts)) md = ["# 联调路径校正映射表(基于 job.youthome.com.cn OpenAPI 实测)", ""] md.append(f"> 生成日期:2026-09-28 | 数据源:`scripts/openapi.json`(182 条真实路径 / 220 path-methods)") md.append(f"> 审计范围:api.ts 内全部 `request()` 调用({len(verdicts)} 个函数)") md.append(f"> 结论:ALIGNED(已对齐)= {c.get('ALIGNED',0)},WRONG→REAL(路径错但有真实等价)= {c.get('WRONG->REAL',0)},PHANTOM(幽灵域,后端无契约)= {c.get('PHANTOM',0)}") md.append("") md.append("## 一、已对齐(无需改路径)") md.append("") md.append("| 函数 | 真实路径 | 原标记 |") md.append("|---|---|---|") for n,(v,rp,note,dai,line) in rows: if v=="ALIGNED": md.append(f"| `{n}` | `{rp}` | {'【待确认】' if dai else '已校正'} |") md.append("") md.append("## 二、路径错但有真实等价端点(需改路径)") md.append("") md.append("| 函数 | 当前(错) | 应改为(真实) | 说明 |") md.append("|---|---|---|---|") for n,(v,rp,note,dai,line) in rows: if v=="WRONG->REAL": curpath = ", ".join(funcs[n]["static"]+funcs[n]["tmpl"]) md.append(f"| `{n}` | `{curpath}` | `{rp}` | {note} |") md.append("") md.append("## 三、幽灵域(后端 OpenAPI 无契约,必须退回 mock-only)") md.append("") md.append("| 函数 | 当前(幽灵)路径 | 说明 |") md.append("|---|---|---|") for n,(v,rp,note,dai,line) in rows: if v=="PHANTOM": curpath = ", ".join(funcs[n]["static"]+funcs[n]["tmpl"]) md.append(f"| `{n}` | `{curpath}` | {note} |") md.append("") with open("docs/联调路径校正映射.md", "w", encoding="utf-8") as f: f.write("\n".join(md)) print("WROTE docs/联调路径校正映射.md")