答辩级 UI 改造:指挥台驾驶舱/证据地图/演化生命线/成长路线图/候选审核台

- 驾驶舱改'人才能力变化指挥台':变化事件流主图+可信度卡+行动卡+播报卡
- 图谱子图同心分层布局、技能变化角标、系统发现折叠入右栏、主操作收敛
- 演化页双 Tab(岗位报告/全岗位扫描)+ 能力生命线 Swimlane + 更替流
- 学习路径成长路线图:概要条/示范路线/预计投入/标记完成联动
- 新增候选审核台抽屉 + GET /api/stats/credibility
- 简历裸姓名/专硕提取修复(桌面真实简历验收通过)+ BGE-M3 加载提示
This commit is contained in:
RoyirinRosmontis
2026-08-19 12:14:39 +08:00
parent ac6d08cdf2
commit 0b198e9976
9 changed files with 1002 additions and 238 deletions
+28
View File
@@ -336,6 +336,34 @@ def graph() -> dict:
# ---------- JD 导入(异步任务) ----------
@app.get("/api/stats/credibility")
def credibility_stats() -> dict:
"""数据可信度指标(驾驶舱「数据可信度」卡):
有效 JD 数 / 独立企业数 / 来源类型数 / 人工复核率 / 异常剔除率。
人工复核率 = 审核留痕数 / (留痕数 + 待审候选数)。"""
jd_dir = resolve(_cfg, "jd_raw_dir")
records, _ = load_jd_dir(jd_dir)
total = len(records)
valid = [r for r in records if not r.is_noise and not r.is_duplicate]
companies = {r.company for r in valid if r.company}
sources = sorted({r.source for r in valid if r.source})
audit = state_db.get_audit(_db_path(), limit=200)
reviewed = sum(1 for r in audit if r.get("kind") != "node_update")
pending = len(load_pending(_db_path()))
denom = reviewed + pending
return _ok({
"jd_total": total,
"jd_valid": len(valid),
"companies": len(companies),
"sources": len(sources),
"source_names": sources,
"reviewed": reviewed,
"pending": pending,
"review_rate": (reviewed / denom) if denom else None,
"excluded_rate": ((total - len(valid)) / total) if total else 0.0,
})
def _run_task(task_id: str) -> None:
task = _state["tasks"][task_id]
try:
+25 -4
View File
@@ -19,9 +19,12 @@ logger = logging.getLogger(__name__)
_PHONE_RE = re.compile(r"1[3-9]\d{9}")
_EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
_NAME_RE = re.compile(r"姓名[::]\s*([一-龥]{2,4})")
# 简历常见写法:首行裸姓名(无"姓名:"前缀),如" 王伟航 "
_BARE_NAME_RE = re.compile(r"^\s*([一-龥]{2,4})\s*$", re.MULTILINE)
_YEARS_RE = re.compile(r"(\d+(?:\.\d+)?)\s*年(?:以上)?[^\n,。;]{0,6}?经验")
_GENDER_RE = re.compile(r"性别[::]?\s*(男|女)")
_EDU_RE = re.compile(r"(博士后|博士|硕士|研究生|MBA|本科|大专|专科)")
_EDU_RE = re.compile(r"(博士后|博士|专硕|学硕|硕士|研究生|MBA|本科|大专|专科)")
_EDU_MAP = {"专硕": "硕士", "学硕": "硕士", "研究生": "硕士"}
_ADDR_RE = re.compile(r"(?:家庭住址|现居(?:住)?(?:地|址)?|联系地址|通讯地址|籍贯)[::]\s*([^\n,。;]{2,30})")
# 常见简历分节标题(整行匹配,可带冒号)
@@ -33,11 +36,28 @@ _SECTION_TITLES = {
}
def _extract_name(text: str) -> str:
"""姓名识别:优先"姓名:X"格式,否则取首个独立的 2-4 字中文裸行(简历常见排版)。"""
m = _NAME_RE.search(text)
if m:
return m.group(1)
for line in text.splitlines()[:5]: # 只在开头几行找,避免误伤正文
b = _BARE_NAME_RE.match(line)
if b:
return b.group(1)
return ""
def mask_text(text: str) -> str:
"""脱敏:手机、邮箱、姓名、住址打码。"""
"""脱敏:手机、邮箱、姓名(含首行裸姓名)、住址打码。"""
text = _PHONE_RE.sub(lambda m: m.group(0)[:3] + "****" + m.group(0)[-2:], text)
text = _EMAIL_RE.sub(lambda m: m.group(0)[0] + "***@" + m.group(0).split("@")[1], text)
text = _NAME_RE.sub(lambda m: "姓名:" + m.group(1)[0] + "*" * (len(m.group(1)) - 1), text)
name = _extract_name(text)
if name: # 裸姓名行整体打码
text = _BARE_NAME_RE.sub(
lambda m: m.group(0).replace(name, name[0] + "*" * (len(name) - 1))
if m.group(1) == name else m.group(0), text, count=1)
text = _ADDR_RE.sub(lambda m: m.group(0)[:2] + m.group(1)[:6] + "***"
if len(m.group(1)) > 6 else m.group(0), text)
return text
@@ -122,6 +142,7 @@ def parse_resume(text: str, matcher: SkillMatcher | None = None,
"""从简历文本解析脱敏画像(含结构化基本信息与分节)。"""
matcher = matcher or SkillMatcher(load_lexicon())
name_m = _NAME_RE.search(text)
name = name_m.group(1) if name_m else _extract_name(text)
phone_m = _PHONE_RE.search(text)
email_m = _EMAIL_RE.search(text)
years_m = _YEARS_RE.search(text)
@@ -130,7 +151,7 @@ def parse_resume(text: str, matcher: SkillMatcher | None = None,
addr_m = _ADDR_RE.search(text)
masked = mask_text(text)
return ResumeProfile(
name=(name_m.group(1)[0] + "*" * (len(name_m.group(1)) - 1)) if name_m else "",
name=(name[0] + "*" * (len(name) - 1)) if name else "",
phone=(phone_m.group(0)[:3] + "****" + phone_m.group(0)[-2:]) if phone_m else "",
email=(email_m.group(0)[0] + "***@" + email_m.group(0).split("@")[1]) if email_m else "",
years=float(years_m.group(1)) if years_m else 0.0,
@@ -138,7 +159,7 @@ def parse_resume(text: str, matcher: SkillMatcher | None = None,
scenarios=matcher.find_scenarios(text),
raw_masked_text=masked,
gender=gender_m.group(1) if gender_m else "",
education=edu_m.group(1) if edu_m else "",
education=_EDU_MAP.get(edu_m.group(1), edu_m.group(1)) if edu_m else "",
address=_mask_addr(addr_m.group(1).strip()) if addr_m else "",
photo_b64=photo_b64,
sections=split_sections(masked),