Initial commit

This commit is contained in:
Z User
2026-06-06 05:21:10 +00:00
Unverified
commit 6664758a6d
493 changed files with 135653 additions and 0 deletions

127
skills/jd-resume-tailor/SKILL.md Executable file
View File

@@ -0,0 +1,127 @@
---
name: jd-resume-tailor
description: 给定一份 JD 和一份现有简历,做"JD 拆解 + 简历定向改写"。拆 JD 抽出硬技能、软技能、加分项;对照简历做 gap 分析;产出针对该岗位重写后的简历,突出相关经验、补齐关键词缺口、并保留候选人真实经历不编造。当用户说"针对这个岗位 / 这家公司改简历""帮我对一下这个 JD""我想投这个职位你看怎么改""把这份简历针对 X 公司优化""做一份定向版简历",或同时给出 JD 文本 + 简历文件时,必须触发本 skill。**请勿用本 skill 做"从零写简历"**——那是 resume-builder 的事。
---
# JD ⇄ Resume TailorJD 拆解 + 简历定向改写)
这个 skill 的边界很窄:**只解决"已有 JD + 已有简历,要改一份命中率最高的版本"**。
不做的事:
- 写新简历(去 `resume-builder`
- 找方向 / 推荐岗位(去 `job-intent-tracker`
- 出面试题(去 `interview-prep`
---
## 何时触发
强信号:
- 用户给了 JD 链接 / JD 文本 + 一份简历 → **必触发**
- "针对这个岗位帮我改简历"
- "对照一下这个 JD"
- "我想投 X 公司的 Y 岗,帮我看简历"
- "做一份定向版"
弱信号(先确认):
- 只给了 JD 没有简历 → 问"你的简历方便发我看一下吗没有的话我可以先帮你从零做一份resume-builder"
- 只给了简历说"改简历" → 问"是针对哪个 JD 改?没有 JD 就用 resume-builder 通用优化"
---
## 工作流
### Step 1: 解析 JD
输入可能是:
- 纯文本(用户粘贴)
- 链接(**不要**自动 fetch提醒用户复制 JD 文本进来;若用户授权 fetch使用 web_fetch
- 截图(用 OCR / 视觉识别,让用户确认抽取结果)
- doc/pdf 文件
调用脚本:
```bash
python scripts/parse_jd.py --jd-file <jd.txt> --out jd_parsed.json
```
脚本会从 JD 抽出:
- **硬技能 must-have**"必须" / "要求" / "至少 X 年" 等强信号词后面的技能)
- **硬技能 nice-to-have**"加分" / "优先" / "熟悉者优先" 等弱信号)
- **软技能信号**(沟通 / 推动 / 跨部门 / 抗压 等)
- **职责动词 + 对象**"负责 X" / "搭建 Y" / "推动 Z"
- **特殊要求**(出差 / 学历 / 证书 / 语言 / 城市)
把结果展示给用户,让用户**确认 / 修正抽取是否准确**(关键 must-have 不能漏)。
### Step 2: 解析简历
输入:用户上传的简历文件(.pdf / .docx / .md / .txt
调用对应 skill 解析:
- pdf → pdf skill
- docx → docx skill
抽出:基本信息、教育、每段工作 / 项目经历的(公司、岗位、时间、职责 bullet、技能列表。
### Step 3: Gap 分析
调用:
```bash
python scripts/jd_gap.py --jd jd_parsed.json --resume resume.txt --out gap.md
```
脚本输出三类清单:
1. **完美命中**JD must-have 在简历里有明确证据)
2. **隐性命中**JD 要求 X简历里有 X 的近义经验,但用词不一样 → 改写时可以"提一下"
3. **真缺口**JD 要求但简历完全没有)
对"真缺口"分两类:
- **可补救**:简历里其实做过类似的事,只是没写出来 → 追问用户"你做过 X 吗?"
- **不可补救**:用户确实没做过 → **不能编**,建议用户在 cover letter 或 summary 里诚实说明并强调 transferable skill
### Step 4: 定向改写
按以下原则重写简历:
**a. 重排经历顺序**:与 JD 最相关的工作 / 项目放最前(不改时间真实性,但可以把项目经历拆成两块"相关项目 / 其他项目"
**b. 重写每条 bullet**
- 把 JD 里的"职责动词"自然嵌入 bullet如 JD 说"主导 ___ 系统设计",简历里就把"参与"改成"主导"——前提是用户确实主导了)
- 数字保留并放大("用户 100 万"是好事,别藏起来)
- 补 JD 关键词(如 JD 说"A/B 测试",但简历里写的是"灰度对比",改成"A/B 测试(灰度对比)"
**c. 重写 Summary**:用 2~3 行总结你为什么是这个岗位的合适人选,**直接对应 JD 的 must-have**
**d. 调整技能列表**:把 JD 提到的技能移到最前面(前提是真的会)
**e. 不改的事实**
- 公司名、岗位名、起止时间、学历 —— 一字不改
- 项目规模、用户量、收入数据 —— 不能编,只能让用户确认后填准
### Step 5: 自检 + 报告
输出三个文件:
1. `resume_tailored_<公司>_<岗位>.md`(改写后的简历)
2. `gap_analysis.md`gap 分析报告)
3. 聊天里给一个 ATS 命中率对比:"改前 X% → 改后 Y%"
附:诚实提醒用户**哪些 bullet 是基于现有信息推测改写的**,让用户复核后再投。
---
## 反模式(不要做)
- ❌ 编造经历("加上一段你没做过的项目"——绝对禁止,哪怕用户要求)
- ❌ 把 JD 的整段话直接粘进简历(很容易被 HR 一眼识破,且 ATS 反作弊会标记)
- ❌ 关键词堆砌在末尾塞一长串技能词凑命中率HR 一眼能看出)
- ❌ 把"参与"改成"主导"但没有问用户实际角色 → 必须先核实
- ❌ 不给用户看 gap自己默默改 → 用户会失去对简历的"理解"
## 与其他 skill 的协作
- 改完后用户说"帮我准备这家公司的面试" → 转 `interview-prep`,把 JD + 改写简历传过去
- 用户说"我想知道还能投哪些类似的岗" → 转 `job-intent-tracker`
- 用户说"我现在简历不太行,能不能整体重做" → 转 `resume-builder`

View File

@@ -0,0 +1,79 @@
# JD 解析信号词参考
## must-have 强信号词
只要 JD 句子里出现这些词,后面的技能 / 经验大概率是硬门槛:
- 必须 / 必备 / 要求 / 应当
- 至少 X 年 / X+ years / 不少于
- "需要" / "需具备"
- "必要条件" / "硬性要求"
- 工科背景 / 985 / 211 / 硕士及以上 / 博士
英文 JD
- Required / Must have / Mandatory / Essential
- Minimum X years
- Strong proficiency in / Expert in
- Demonstrated experience with
## nice-to-have 弱信号词
- 加分 / 优先 / 优先考虑
- 熟悉 ___ 者优先
- 有 ___ 经验者优先
- "了解 ___ 即可"
- 加分项
英文:
- Preferred / Nice to have / Plus / Bonus / Desirable
- Familiar with / Exposure to
- A plus / Would be a plus
## 职责动词(写在 bullet 里能呼应 JD
中文:负责、主导、推动、设计、搭建、优化、规划、迭代、孵化、复盘、运营、管理、协调、执行
英文Lead / Drive / Build / Design / Architect / Develop / Implement / Optimize / Manage / Coordinate / Execute / Own
## 特殊要求
- **学历**:本科 / 硕士 / 博士及以上;学校 tier
- **工作年限**X-Y 年(写明 range不写满则有弹性
- **语言**:英语口语流利 / CET-6 / 雅思 X / 母语
- **证书**CFA / CPA / PMP / AWS Solutions Architect 等
- **出差 / 派驻 / 加班**"接受出差"、"奇偶周末调休"、"项目制 996"
- **工作地点**:城市 + 是否 remote / hybrid
## 反信号(看到这些要警觉)
- "其他领导交办的任务" → 工作边界模糊
- "良好的抗压能力" → 加班多
- "拥抱变化 / 快速迭代" → 业务方向不稳定
- "扁平化沟通 / 没有层级" → 实际可能更乱
- "5 险一金 + 节日福利" 写在 JD 显眼位置 → 福利可能就这些
## 输出格式Step 1 给用户看的)
```json
{
"company": "XX 公司",
"position": "XX 岗位",
"must_have": [
{"item": "5 年以上 C 端产品经验", "evidence": "JD 第 X 行"},
{"item": "熟练 SQL", "evidence": "JD 第 Y 行"}
],
"nice_to_have": [
{"item": "海外业务经验", "evidence": "JD 第 Z 行"}
],
"soft_skills": ["跨部门推动", "数据驱动决策"],
"responsibilities": [
"主导 ___ 业务线产品规划",
"通过数据分析驱动迭代"
],
"special_requirements": {
"education": "本科及以上",
"years": "5+",
"language": "英语口语流利",
"location": "上海,可接受短期出差"
}
}
```

View File

@@ -0,0 +1,75 @@
# 简历定向改写原则
## 核心原则
### 1. 不编造,只重组
简历里的事实部分(公司、岗位、时间、量化数字)**一字不改**。
可以改的:
- bullet 的措辞和顺序
- 强调的角度(同一段经历,不同 JD 强调不同侧面)
- 项目分类("相关项目"放前,"其他项目"放后)
### 2. 把 JD 的关键词嵌入到真实经验里,而不是单独贴
❌ 错:在简历末尾加一行"关键词A/B 测试、用户增长、SQL、Python"
✅ 对:把"我做过的灰度对比"改成"A/B 测试(灰度对比)",让关键词嵌在真实场景里
### 3. 用 JD 的动词
JD 说"主导 ___ 系统设计",如果你是这段经历的负责人,把"参与"改成"主导"。
但**前提是真的主导**——agent 在改写前必须确认:
- 这段经历你是 leader 吗?是的话改"主导";不是就保留"参与"或写明"作为 _ 角色 协助 ___"
### 4. 重排经历顺序
工作经历(按时间倒序)—— 不可改,否则失实。
项目经历 / 实习经历 —— 可以改,把与 JD 强相关的放前面。
更进一步:可以拆成两个小标题:
```
【相关项目】(与目标岗位强相关)
- ...
【其他项目】
- ...
```
### 5. Summary 要直接对位 JD must-have
JD 说"5 年 C 端产品 + SQL + A/B 测试"
Summary 改成:"5 年 C 端互联网产品经验,主导过 ___ 增长项目,熟练使用 SQL 与 A/B 测试驱动决策"
### 6. 技能列表也要倾斜
JD 说"Python / Spark / Flink",简历技能列表把这三个放前面。
不会的不要写。
### 7. 适度补 nice-to-have
如果 JD 的 nice-to-have 你**真的有**但简历没写,补上。
如果没有不补——nice-to-have 本来就不是硬门槛,不影响过简历筛选。
## 改写后的双向校验
每条改后 bullet 自问:
- 这是不是用户真实做过的?
- 这条 bullet 在 JD 里能找到对应的关键词 / 职责吗?
- 量化数字有保留吗?
每段经历自问:
- 是否覆盖了 JD 的至少 1 条 must-have
- 是否突出了用户在这段经历里"真正的高光"
整份简历自问:
- ATS 命中率有提升吗?
- HR 看 30 秒能不能 get 到"为什么这个候选人适合这个岗位"
- 有没有让真实经历变形 / 失实?
## 当用户与 JD 严重不匹配时
诚实告诉用户:
- "JD 里有 N 条 must-have 你的简历完全没有覆盖,这个岗位投递成功率较低"
- "建议你考虑:(a) 投匹配度更高的类似岗位 (b) 在 cover letter 里诚实说明 transferable skill (c) 先补技能再投"
不要为了"看起来匹配"而扭曲简历。

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env python3
"""
jd_gap.py — 把 parse_jd.py 的 JSON 与简历文本做 gap 分析
用法:
python jd_gap.py --jd jd_parsed.json --resume resume.md --out gap.md
输出 markdown 报告:完美命中 / 隐性命中 / 真缺口 三类,附改写建议。
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
def load_resume_text(path: Path) -> str:
suffix = path.suffix.lower()
if suffix in {".md", ".txt"}:
return path.read_text(encoding="utf-8")
if suffix == ".docx":
try:
from docx import Document
except ImportError:
print(
"✗ 缺少 python-docxpip install python-docx --break-system-packages",
file=sys.stderr,
)
sys.exit(1)
doc = Document(str(path))
return "\n".join(p.text for p in doc.paragraphs)
print(f"✗ 暂不支持的格式:{suffix}", file=sys.stderr)
sys.exit(1)
def find_evidence(resume_text: str, keyword: str, window: int = 30) -> str | None:
"""在简历里找关键词,返回上下文片段;找不到返回 None。"""
pattern = re.escape(keyword)
m = re.search(pattern, resume_text, flags=re.IGNORECASE)
if not m:
return None
start = max(0, m.start() - window)
end = min(len(resume_text), m.end() + window)
snippet = resume_text[start:end].replace("\n", " ").strip()
return snippet
def fuzzy_hit(resume_text: str, keyword: str) -> str | None:
"""模糊命中:取关键词的中文 / 英文核心,做包含匹配。"""
# 拿前 2 个字 / 前 5 个字符
candidates = []
if re.search(r"[一-龥]", keyword):
if len(keyword) >= 4:
candidates.append(keyword[:2])
candidates.append(keyword[-2:])
else:
if len(keyword) >= 4:
candidates.append(keyword[:4].lower())
text_low = resume_text.lower()
for c in candidates:
if c and c in text_low:
return c
return None
def analyze(jd: dict, resume_text: str) -> dict:
perfect, implicit, missing = [], [], []
# 用 must_have 句子里抽出来的 skills 作为对比项
candidates = jd.get("skills_extracted", []) + jd.get("must_have", [])
seen = set()
for c in candidates:
# 句子层面太长,截短
keyword = c.strip()
if len(keyword) > 30:
# 从长句子里抽更短的关键词
short_tokens = re.findall(
r"[A-Za-z][A-Za-z0-9+/.\-_]{1,20}|[一-龥]{2,6}",
keyword,
)
for t in short_tokens:
if t.lower() not in seen:
seen.add(t.lower())
process_one(t, resume_text, perfect, implicit, missing)
else:
if keyword.lower() not in seen:
seen.add(keyword.lower())
process_one(keyword, resume_text, perfect, implicit, missing)
return {"perfect": perfect, "implicit": implicit, "missing": missing}
def process_one(keyword, resume_text, perfect, implicit, missing):
ev = find_evidence(resume_text, keyword)
if ev:
perfect.append({"keyword": keyword, "evidence": ev})
return
fuzzy = fuzzy_hit(resume_text, keyword)
if fuzzy:
implicit.append({"keyword": keyword, "fuzzy_match": fuzzy})
else:
missing.append(keyword)
def render(jd: dict, gap: dict) -> str:
lines = ["# JD ⇄ Resume Gap 分析报告", ""]
spec = jd.get("special_requirements", {})
if spec:
lines += ["## JD 硬条件", ""]
for k, v in spec.items():
lines.append(f"- **{k}**{v}")
lines.append("")
lines += ["## ✅ 完美命中(简历里有明确证据)", ""]
if gap["perfect"]:
for item in gap["perfect"][:30]:
lines.append(f"- **{item['keyword']}** —— 证据:`...{item['evidence']}...`")
else:
lines.append("(无)")
lines.append("")
lines += ["## 🟡 隐性命中(简历里有近似但用词不同,建议改写时对齐)", ""]
if gap["implicit"]:
for item in gap["implicit"][:20]:
lines.append(f"- **JD 关键词:{item['keyword']}**(简历里出现:`{item['fuzzy_match']}`")
else:
lines.append("(无)")
lines.append("")
lines += ["## 🔴 真缺口(简历完全没有,需要确认 / 补充 / 转换叙事)", ""]
if gap["missing"]:
for kw in gap["missing"][:30]:
lines.append(f"- **{kw}**")
else:
lines.append("(无)")
lines.append("")
lines += [
"---",
"## 改写建议",
"",
"1. **完美命中** 的部分保留,但确保措辞与 JD 一致(比如 JD 用『A/B 测试』就别写『AB 实验』)",
"2. **隐性命中** 是性价比最高的优化点 —— 把简历里的近义词改成 JD 的措辞",
"3. **真缺口** 分两类:",
" - 你做过但没写?→ 补到对应经历的 bullet 里",
" - 你没做过?→ **不要编造**。可以在 cover letter / Summary 里诚实说明 transferable skill",
"4. 把改后的简历再跑一次 ats_check.py 看命中率是否提升",
]
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--jd", required=True, help="parse_jd.py 输出的 json")
parser.add_argument("--resume", required=True, help="简历文件 (.md/.txt/.docx)")
parser.add_argument("--out", help="输出 markdown 路径")
args = parser.parse_args()
jd = json.loads(Path(args.jd).read_text(encoding="utf-8"))
resume_text = load_resume_text(Path(args.resume))
gap = analyze(jd, resume_text)
report = render(jd, gap)
if args.out:
Path(args.out).write_text(report, encoding="utf-8")
print(f"✓ Gap 报告已生成:{args.out}")
else:
print(report)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""
parse_jd.py — 解析 JD 文本,抽取 must-have / nice-to-have / 职责 / 特殊要求
用法:
python parse_jd.py --jd-file jd.txt --out jd_parsed.json
python parse_jd.py --jd-text "..." --out jd_parsed.json
输出 JSON 结构供下一步的 jd_gap.py 使用,也可以直接给用户看。
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
MUST_PATTERNS = [
r"必须", r"必备", r"必要条件", r"硬性要求", r"应当", r"需要", r"需具备",
r"至少\s*\d+\s*年", r"\d+\+?\s*年以上",
r"required", r"must\s*have", r"mandatory", r"essential",
r"minimum\s+\d+\s+years",
]
NICE_PATTERNS = [
r"加分", r"优先", r"加分项", r"熟悉.+者优先", r"有.+经验者优先",
r"preferred", r"nice\s*to\s*have", r"plus", r"bonus", r"desirable",
]
ACTION_VERBS = [
"负责", "主导", "推动", "设计", "搭建", "构建", "优化", "规划", "迭代",
"孵化", "复盘", "运营", "管理", "协调", "执行", "驱动", "落地", "重构",
"lead", "drive", "build", "design", "architect", "develop", "implement",
"optimize", "manage", "coordinate", "execute", "own",
]
def split_sentences(text: str) -> list[str]:
# 中文按 。!?; 拆,英文按 . ; 拆,并保留 bullet 行
raw = re.split(r"[。!?!?;\n]+", text)
return [s.strip(" \t-•·*") for s in raw if s.strip()]
def classify_sentences(sentences: list[str]) -> dict:
must, nice, resp, others = [], [], [], []
for s in sentences:
s_low = s.lower()
if any(re.search(p, s_low) for p in NICE_PATTERNS):
nice.append(s)
elif any(re.search(p, s_low) for p in MUST_PATTERNS):
must.append(s)
elif any(v in s_low for v in ACTION_VERBS):
resp.append(s)
else:
others.append(s)
return {"must": must, "nice": nice, "responsibilities": resp, "others": others}
def extract_special(text: str) -> dict:
out: dict[str, str] = {}
# 学历
edu = re.search(r"(本科|硕士|博士|大专)(?:及以上|以上)?", text)
if edu:
out["education"] = edu.group(0)
# 工作年限
years = re.search(r"(\d+)\s*[-~–到至]\s*(\d+)\s*年|(\d+)\s*\+?\s*年(以上|及以上)?", text)
if years:
out["years"] = years.group(0)
# 语言
lang_pat = re.search(
r"(英语\s*(口语)?\s*(流利|熟练|母语)|CET[-\s]?[46]|雅思\s*\d(\.\d)?|托福\s*\d{2,3}|母语水平|business\s*english)",
text,
flags=re.IGNORECASE,
)
if lang_pat:
out["language"] = lang_pat.group(0)
# 城市
cities = re.findall(
r"(北京|上海|广州|深圳|杭州|南京|苏州|成都|武汉|西安|香港|新加坡|remote|hybrid|远程|海外)",
text,
flags=re.IGNORECASE,
)
if cities:
out["location"] = "/".join(sorted(set(c.lower() for c in cities)))
# 出差 / 加班信号
travel = re.search(r"(出差|派驻|常驻|项目制|加班|999|996|大小周)", text)
if travel:
out["working_style"] = travel.group(0)
# 证书
certs = re.findall(
r"(CFA(?:\s*Level\s*[I123]+)?|CPA|FRM|ACCA|PMP|AWS\s*[\w\s]*认证|Azure\s*[\w]*|GCP\s*[\w]*)",
text,
flags=re.IGNORECASE,
)
if certs:
out["certificates"] = "/".join(sorted(set(c.strip() for c in certs)))
return out
def extract_skills(sentences: list[str]) -> list[str]:
"""从所有句子里抽取技能词候选(短词优先,避免抽出整句)。"""
text = " ".join(sentences)
# 英文技能CamelCase 或大写开头的词、含 . 或 +/- 的标识)
en = re.findall(r"\b[A-Za-z][A-Za-z0-9+/.\-_#]{1,20}\b", text)
# 中文 2~5 字常见技能词
zh = re.findall(r"[一-龥]{2,5}", text)
raw = en + zh
stop = {
# 中文虚词 / 通用动词
"公司", "我们", "你将", "团队", "需要", "能够", "具备", "熟悉", "了解",
"良好", "优秀", "经验", "能力", "岗位", "职责", "要求", "以上", "相关",
"进行", "完成", "负责", "推动", "实现", "提升", "并且", "包括", "以下",
"工作", "项目", "业务", "及其", "或者", "", "", "", "", "",
"", "", "", "", "", "等等", "通过", "", "", "", "",
"至上", "本科", "硕士", "博士", "者优先", "根据", "进行", "支持", "参与",
"主导", "提供", "建立", "搭建", "设计", "驱动", "决策", "分析", "推动",
"迭代", "规划", "运营", "协作", "跨部门", "跨团队", "高级", "资深",
"若干", "多种", "多元", "多类",
# 英文虚词
"and", "the", "with", "for", "of", "or", "to", "be", "as", "an", "is",
"are", "in", "on", "at", "by", "all", "you", "we", "us", "our", "your",
"a", "an", "this", "that", "these", "those", "it", "its",
}
# 含数字的"X年""X个"也过滤
digit_only = re.compile(r"^\d+$")
seen = set()
out = []
for token in raw:
key = token.lower()
if key in seen or token in stop or len(token) < 2 or digit_only.match(token):
continue
# 中文 token 不允许全是 stop 词的子串
seen.add(key)
out.append(token)
return out[:60]
def main() -> None:
parser = argparse.ArgumentParser()
src = parser.add_mutually_exclusive_group(required=True)
src.add_argument("--jd-file", help="JD 文本文件路径")
src.add_argument("--jd-text", help="直接传 JD 文本")
parser.add_argument("--out", help="输出 JSON 路径,缺省打印")
args = parser.parse_args()
if args.jd_file:
path = Path(args.jd_file).expanduser()
if not path.exists():
print(f"✗ JD 文件不存在:{path}", file=sys.stderr)
sys.exit(1)
text = path.read_text(encoding="utf-8")
else:
text = args.jd_text
sentences = split_sentences(text)
classified = classify_sentences(sentences)
special = extract_special(text)
skills = extract_skills(classified["must"] + classified["responsibilities"])
result = {
"must_have": classified["must"],
"nice_to_have": classified["nice"],
"responsibilities": classified["responsibilities"],
"skills_extracted": skills,
"special_requirements": special,
"raw_sentence_count": len(sentences),
}
payload = json.dumps(result, ensure_ascii=False, indent=2)
if args.out:
out_path = Path(args.out).expanduser()
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(payload, encoding="utf-8")
print(f"✓ JD 解析结果已保存:{out_path}")
else:
print(payload)
if __name__ == "__main__":
main()