knowme-careerforge-skill 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -9
- package/README.zh-CN.md +6 -9
- package/SKILL.md +95 -116
- package/agents/claude/knowme-careerforge.md +9 -8
- package/agents/codex/knowme-careerforge.yaml +15 -6
- package/agents/cursor/knowme-careerforge.mdc +7 -6
- package/agents/opencode/skill.yaml +1 -1
- package/agents/windsurf/knowme-careerforge.rules +11 -8
- package/dist/cli/src/commands/init.d.ts +1 -1
- package/dist/cli/src/commands/init.js +37 -10
- package/dist/cli/src/index.js +65 -22
- package/dist/scripts/render-pdf.d.ts +0 -9
- package/dist/scripts/render-pdf.js +5 -68
- package/dist/scripts/rendering/render-pdf.d.ts +11 -0
- package/dist/scripts/rendering/render-pdf.js +109 -0
- package/dist/scripts/validate-ats.d.ts +0 -21
- package/dist/scripts/validate-ats.js +5 -132
- package/dist/scripts/validate-layout.d.ts +0 -22
- package/dist/scripts/validate-layout.js +5 -124
- package/dist/scripts/validation/validate-ats.d.ts +23 -0
- package/dist/scripts/validation/validate-ats.js +173 -0
- package/dist/scripts/validation/validate-layout.d.ts +24 -0
- package/dist/scripts/validation/validate-layout.js +164 -0
- package/package.json +15 -12
- package/scripts/Agent.md +138 -0
- package/scripts/{build-gallery.py → build/build-gallery.py} +128 -99
- package/scripts/{build-knowledge.py → build/build-knowledge.py} +9 -1
- package/scripts/{release.py → build/release.py} +20 -16
- package/scripts/{run-all-tests.py → build/run-all-tests.py} +9 -1
- package/scripts/build/sync-version.py +107 -0
- package/scripts/{analyze-jd.py → evidence/analyze-jd.py} +34 -45
- package/scripts/evidence/extract-evidence.py +348 -0
- package/scripts/pipeline/forge.py +142 -0
- package/scripts/rendering/render-pdf.py +162 -0
- package/scripts/{render-pdf.ts → rendering/render-pdf.ts} +3 -3
- package/scripts/template/instantiate-resume.py +178 -0
- package/scripts/{search-template.py → template/search-template.py} +44 -42
- package/scripts/{validate-ats.ts → validation/validate-ats.ts} +39 -35
- package/scripts/{validate-layout.ts → validation/validate-layout.ts} +43 -42
- package/scripts/{validate-resume.py → validation/validate-resume.py} +31 -22
- package/skill.json +1 -1
- package/src/knowledge/index.json +1 -1
- package/src/knowledge/resume-schema.json +112 -0
- package/src/workflows/forge-resume.md +2 -2
- package/src/workflows/review.md +3 -3
- package/scripts/instantiate-resume.py +0 -83
- package/scripts/sync-version.py +0 -57
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
KnowMe CareerForge — Unified End-to-End Forge Engine
|
|
4
|
+
一键打通:代码仓事实挖掘 (repo-to-resume) ➔ JD 深度解析 ➔ 多维模板检索 ➔ HTML 工作区装配 ➔ Dual-QA 质检 ➔ 确定性 PDF 导出。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys, os, json, argparse, subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
def get_project_root() -> Path:
|
|
11
|
+
curr = Path(__file__).resolve().parent
|
|
12
|
+
for _ in range(5):
|
|
13
|
+
if (curr / "SKILL.md").exists() or (curr / "package.json").exists():
|
|
14
|
+
return curr
|
|
15
|
+
curr = curr.parent
|
|
16
|
+
return Path.cwd()
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
parser = argparse.ArgumentParser(description="KnowMe CareerForge — One-Shot Unified Forge Engine")
|
|
20
|
+
parser.add_argument("--repo", "-r", default=".", help="Target code repository path (default: current directory)")
|
|
21
|
+
parser.add_argument("--role", help="Target job title / role (e.g. 'AI Agent Engineer', '资深前端专家')")
|
|
22
|
+
parser.add_argument("--jd", help="Path to JD file or raw JD text")
|
|
23
|
+
parser.add_argument("--template", "-t", default="minimal", help="Template ID (minimal, modern, executive, classic)")
|
|
24
|
+
parser.add_argument("--name", help="Candidate full name override")
|
|
25
|
+
parser.add_argument("--email", help="Candidate email override")
|
|
26
|
+
parser.add_argument("--phone", help="Candidate phone override")
|
|
27
|
+
parser.add_argument("--output", "-o", default="workspace/resume.pdf", help="Output PDF file path")
|
|
28
|
+
parser.add_argument("--html-output", default="workspace/resume.html", help="Intermediate working canvas HTML path")
|
|
29
|
+
parser.add_argument("--quiet", "-q", action="store_true", help="Quiet mode (compact JSON output for AI Agent)")
|
|
30
|
+
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
root = get_project_root()
|
|
33
|
+
scripts_dir = root / "scripts"
|
|
34
|
+
|
|
35
|
+
if not args.quiet:
|
|
36
|
+
print("==============================================================")
|
|
37
|
+
print(" KnowMe CareerForge — One-Shot Resume Engineering Pipeline")
|
|
38
|
+
print(f" Target Role : {args.role or 'Auto-Detect'}")
|
|
39
|
+
print(f" Repo Source : {args.repo}")
|
|
40
|
+
print(f" Template : {args.template}")
|
|
41
|
+
print("==============================================================")
|
|
42
|
+
|
|
43
|
+
# 1. 证据挖掘 (Stage 1: Evidence Mining)
|
|
44
|
+
evidence_json = Path("workspace/evidence-master.json")
|
|
45
|
+
|
|
46
|
+
extract_script = scripts_dir / "evidence" / "extract-evidence.py"
|
|
47
|
+
if not extract_script.exists():
|
|
48
|
+
extract_script = scripts_dir / "extract-evidence.py"
|
|
49
|
+
|
|
50
|
+
evidence_cmd = [sys.executable, str(extract_script), "--repo", args.repo, "--output", str(evidence_json), "--quiet"]
|
|
51
|
+
if args.name: evidence_cmd.extend(["--name", args.name])
|
|
52
|
+
if args.role: evidence_cmd.extend(["--role", args.role])
|
|
53
|
+
if args.email: evidence_cmd.extend(["--email", args.email])
|
|
54
|
+
if args.phone: evidence_cmd.extend(["--phone", args.phone])
|
|
55
|
+
|
|
56
|
+
subprocess.run(evidence_cmd, check=True)
|
|
57
|
+
if not args.quiet:
|
|
58
|
+
print("[✓] Stage 1: Codebase evidence mined -> workspace/evidence-master.json")
|
|
59
|
+
|
|
60
|
+
# 2. JD 分析与关键词提取 (Stage 2: JD Analysis)
|
|
61
|
+
keywords = []
|
|
62
|
+
if args.jd:
|
|
63
|
+
try:
|
|
64
|
+
analyze_script = scripts_dir / "evidence" / "analyze-jd.py"
|
|
65
|
+
if not analyze_script.exists():
|
|
66
|
+
analyze_script = scripts_dir / "analyze-jd.py"
|
|
67
|
+
|
|
68
|
+
jd_cmd = [sys.executable, str(analyze_script)]
|
|
69
|
+
if Path(args.jd).exists():
|
|
70
|
+
jd_cmd.extend(["--jd", args.jd])
|
|
71
|
+
else:
|
|
72
|
+
jd_cmd.extend(["--text", args.jd])
|
|
73
|
+
jd_res = subprocess.run(jd_cmd, stdout=subprocess.PIPE, text=True, check=True)
|
|
74
|
+
for line in jd_res.stdout.splitlines():
|
|
75
|
+
if "Must-have Skills" in line or "Detected" in line:
|
|
76
|
+
parts = line.split(":")[-1].strip().split(",")
|
|
77
|
+
keywords.extend([p.strip() for p in parts if p.strip()])
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
# 3. 模板装配到工作区 (Stage 3 & 4: Template Instantiation)
|
|
82
|
+
kw_arg = ",".join(keywords[:8]) if keywords else "TypeScript,Playwright,Python,Docker"
|
|
83
|
+
|
|
84
|
+
instantiate_script = scripts_dir / "template" / "instantiate-resume.py"
|
|
85
|
+
if not instantiate_script.exists():
|
|
86
|
+
instantiate_script = scripts_dir / "instantiate-resume.py"
|
|
87
|
+
|
|
88
|
+
instantiate_cmd = [
|
|
89
|
+
sys.executable, str(instantiate_script),
|
|
90
|
+
"--template", args.template,
|
|
91
|
+
"--profile", str(evidence_json),
|
|
92
|
+
"--keywords", kw_arg,
|
|
93
|
+
"--output", args.html_output
|
|
94
|
+
]
|
|
95
|
+
subprocess.run(instantiate_cmd, stdout=subprocess.DEVNULL if args.quiet else None, check=True)
|
|
96
|
+
if not args.quiet:
|
|
97
|
+
print(f"[✓] Stage 2: HTML Working Canvas instantiated -> {args.html_output}")
|
|
98
|
+
|
|
99
|
+
# 4. 质检与自愈校验 (Stage 5: Dual QA Validation)
|
|
100
|
+
val_script = scripts_dir / "validation" / "validate-resume.py"
|
|
101
|
+
if not val_script.exists():
|
|
102
|
+
val_script = scripts_dir / "validate-resume.py"
|
|
103
|
+
|
|
104
|
+
validator_cmd = [
|
|
105
|
+
sys.executable, str(val_script),
|
|
106
|
+
args.html_output
|
|
107
|
+
]
|
|
108
|
+
subprocess.run(validator_cmd, stdout=subprocess.DEVNULL if args.quiet else None, check=True)
|
|
109
|
+
if not args.quiet:
|
|
110
|
+
print("[✓] Stage 3: Dual QA Validation Passed 100%")
|
|
111
|
+
|
|
112
|
+
# 5. 确定性渲染导出 PDF (Stage 6: PDF Export)
|
|
113
|
+
render_script = scripts_dir / "rendering" / "render-pdf.py"
|
|
114
|
+
if not render_script.exists():
|
|
115
|
+
render_script = scripts_dir / "render-pdf.py"
|
|
116
|
+
|
|
117
|
+
render_cmd = [
|
|
118
|
+
sys.executable, str(render_script),
|
|
119
|
+
args.html_output, args.output
|
|
120
|
+
]
|
|
121
|
+
if args.quiet:
|
|
122
|
+
render_cmd.append("--quiet")
|
|
123
|
+
subprocess.run(render_cmd, check=True)
|
|
124
|
+
|
|
125
|
+
if not args.quiet:
|
|
126
|
+
print("==============================================================")
|
|
127
|
+
print(f" [✓] ALL STAGES COMPLETED SUCCESSFULLY!")
|
|
128
|
+
print(f" HTML Working Canvas : {Path(args.html_output).resolve()}")
|
|
129
|
+
print(f" Final Verified PDF : {Path(args.output).resolve()}")
|
|
130
|
+
print("==============================================================")
|
|
131
|
+
else:
|
|
132
|
+
result = {
|
|
133
|
+
"status": "SUCCESS",
|
|
134
|
+
"htmlCanvas": str(Path(args.html_output).resolve()),
|
|
135
|
+
"pdfDelivery": str(Path(args.output).resolve()),
|
|
136
|
+
"evidenceProfile": str(evidence_json.resolve()),
|
|
137
|
+
"templateUsed": args.template
|
|
138
|
+
}
|
|
139
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
main()
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
KnowMe CareerForge — Multi-Strategy Deterministic PDF Renderer
|
|
4
|
+
多策略自愈式 A4 PDF 渲染器:自动嗅探 Playwright、系统级 Chromium/Chrome/Edge/Brave 浏览器并实现零配置确定性导出。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys, os, subprocess, platform, shutil, argparse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, List
|
|
10
|
+
|
|
11
|
+
def get_project_root() -> Path:
|
|
12
|
+
curr = Path(__file__).resolve().parent
|
|
13
|
+
for _ in range(5):
|
|
14
|
+
if (curr / "SKILL.md").exists() or (curr / "package.json").exists():
|
|
15
|
+
return curr
|
|
16
|
+
curr = curr.parent
|
|
17
|
+
return Path.cwd()
|
|
18
|
+
|
|
19
|
+
def find_system_browser() -> Optional[str]:
|
|
20
|
+
system = platform.system()
|
|
21
|
+
|
|
22
|
+
custom_bin = os.environ.get("CHROME_BIN") or os.environ.get("BROWSER_PATH")
|
|
23
|
+
if custom_bin and Path(custom_bin).exists():
|
|
24
|
+
return custom_bin
|
|
25
|
+
|
|
26
|
+
candidates: List[str] = []
|
|
27
|
+
|
|
28
|
+
if system == "Darwin": # macOS
|
|
29
|
+
candidates = [
|
|
30
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
31
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
32
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
33
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
34
|
+
str(Path.home() / "Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
|
|
35
|
+
str(Path.home() / "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"),
|
|
36
|
+
]
|
|
37
|
+
elif system == "Linux":
|
|
38
|
+
candidates = [
|
|
39
|
+
"google-chrome", "google-chrome-stable", "chromium", "chromium-browser",
|
|
40
|
+
"/usr/bin/google-chrome", "/usr/bin/google-chrome-stable",
|
|
41
|
+
"/usr/bin/chromium", "/usr/bin/chromium-browser",
|
|
42
|
+
"/snap/bin/chromium"
|
|
43
|
+
]
|
|
44
|
+
elif system == "Windows":
|
|
45
|
+
prog_files = os.environ.get("PROGRAMFILES", r"C:\Program Files")
|
|
46
|
+
prog_files_x86 = os.environ.get("PROGRAMFILES(X86)", r"C:\Program Files (x86)")
|
|
47
|
+
local_app = os.environ.get("LOCALAPPDATA", "")
|
|
48
|
+
|
|
49
|
+
candidates = [
|
|
50
|
+
os.path.join(prog_files, r"Google\Chrome\Application\chrome.exe"),
|
|
51
|
+
os.path.join(prog_files_x86, r"Google\Chrome\Application\chrome.exe"),
|
|
52
|
+
os.path.join(prog_files_x86, r"Microsoft\Edge\Application\msedge.exe"),
|
|
53
|
+
os.path.join(prog_files, r"Microsoft\Edge\Application\msedge.exe"),
|
|
54
|
+
os.path.join(local_app, r"Google\Chrome\Application\chrome.exe"),
|
|
55
|
+
os.path.join(local_app, r"Microsoft\Edge\Application\msedge.exe"),
|
|
56
|
+
os.path.join(local_app, r"BraveSoftware\Brave-Browser\Application\brave.exe"),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
for candidate in candidates:
|
|
60
|
+
if candidate.startswith("/") or ":" in candidate:
|
|
61
|
+
if Path(candidate).exists():
|
|
62
|
+
return candidate
|
|
63
|
+
else:
|
|
64
|
+
resolved = shutil.which(candidate)
|
|
65
|
+
if resolved:
|
|
66
|
+
return resolved
|
|
67
|
+
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def render_via_playwright(html_path: Path, pdf_path: Path) -> bool:
|
|
71
|
+
root = get_project_root()
|
|
72
|
+
script_ts = root / "scripts" / "rendering" / "render-pdf.ts"
|
|
73
|
+
if not script_ts.exists():
|
|
74
|
+
script_ts = root / "scripts" / "render-pdf.ts"
|
|
75
|
+
|
|
76
|
+
if script_ts.exists() and shutil.which("npx"):
|
|
77
|
+
try:
|
|
78
|
+
cmd = ["npx", "ts-node", str(script_ts), "--input", str(html_path), "--output", str(pdf_path)]
|
|
79
|
+
res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=30)
|
|
80
|
+
if res.returncode == 0 and pdf_path.exists():
|
|
81
|
+
return True
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
def render_via_browser(browser_bin: str, html_path: Path, pdf_path: Path) -> bool:
|
|
87
|
+
html_url = html_path.resolve().as_uri()
|
|
88
|
+
cmd = [
|
|
89
|
+
browser_bin,
|
|
90
|
+
"--headless=new",
|
|
91
|
+
"--disable-gpu",
|
|
92
|
+
"--no-first-run",
|
|
93
|
+
"--no-default-browser-check",
|
|
94
|
+
"--disable-background-networking",
|
|
95
|
+
"--no-pdf-header-footer",
|
|
96
|
+
"--run-all-compositor-stages-before-draw",
|
|
97
|
+
f"--print-to-pdf={pdf_path.resolve()}",
|
|
98
|
+
html_url
|
|
99
|
+
]
|
|
100
|
+
try:
|
|
101
|
+
res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=25)
|
|
102
|
+
return pdf_path.exists() and pdf_path.stat().st_size > 0
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"[!] Browser render failed: {e}")
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
def render_pdf(html_path_str: str, pdf_path_str: str, quiet: bool = False) -> Path:
|
|
108
|
+
html_path = Path(html_path_str).resolve()
|
|
109
|
+
pdf_path = Path(pdf_path_str).resolve()
|
|
110
|
+
|
|
111
|
+
if not html_path.exists():
|
|
112
|
+
raise FileNotFoundError(f"Input HTML file does not exist: {html_path}")
|
|
113
|
+
|
|
114
|
+
pdf_path.parent.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
|
|
116
|
+
if not quiet:
|
|
117
|
+
print("==============================================================")
|
|
118
|
+
print(" KnowMe CareerForge — Multi-Strategy PDF Renderer")
|
|
119
|
+
print(f" Source Canvas : {html_path}")
|
|
120
|
+
print(f" Target PDF : {pdf_path}")
|
|
121
|
+
print("==============================================================")
|
|
122
|
+
|
|
123
|
+
# Strategy 1: Playwright Headless
|
|
124
|
+
if not quiet:
|
|
125
|
+
print("[*] Strategy 1: Trying Playwright Headless Engine...")
|
|
126
|
+
if render_via_playwright(html_path, pdf_path):
|
|
127
|
+
if not quiet:
|
|
128
|
+
print(f"[✓] Render Succeeded via Playwright -> {pdf_path}")
|
|
129
|
+
return pdf_path
|
|
130
|
+
|
|
131
|
+
# Strategy 2: System Installed Chromium/Chrome/Edge/Brave
|
|
132
|
+
browser_bin = find_system_browser()
|
|
133
|
+
if browser_bin:
|
|
134
|
+
if not quiet:
|
|
135
|
+
print(f"[*] Strategy 2: Found system browser at: {browser_bin}")
|
|
136
|
+
if render_via_browser(browser_bin, html_path, pdf_path):
|
|
137
|
+
if not quiet:
|
|
138
|
+
print(f"[✓] Render Succeeded via System Browser -> {pdf_path}")
|
|
139
|
+
return pdf_path
|
|
140
|
+
|
|
141
|
+
raise RuntimeError("No suitable PDF rendering engine found. Please install Playwright or Google Chrome / Edge.")
|
|
142
|
+
|
|
143
|
+
def main():
|
|
144
|
+
parser = argparse.ArgumentParser(description="KnowMe CareerForge — Multi-Strategy PDF Renderer")
|
|
145
|
+
parser.add_argument("html", nargs="?", default="workspace/resume.html", help="Input HTML canvas path")
|
|
146
|
+
parser.add_argument("pdf", nargs="?", default="workspace/resume.pdf", help="Output PDF path")
|
|
147
|
+
parser.add_argument("--input", "-i", help="Input HTML (named option)")
|
|
148
|
+
parser.add_argument("--output", "-o", help="Output PDF (named option)")
|
|
149
|
+
parser.add_argument("--quiet", "-q", action="store_true", help="Quiet execution mode")
|
|
150
|
+
|
|
151
|
+
args = parser.parse_args()
|
|
152
|
+
input_html = args.input or args.html
|
|
153
|
+
output_pdf = args.output or args.pdf
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
render_pdf(input_html, output_pdf, quiet=args.quiet)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
print(f"[✗] PDF Generation Failed: {e}", file=sys.stderr)
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
main()
|
|
@@ -21,7 +21,6 @@ export async function renderResumePdf(options: RenderOptions): Promise<void> {
|
|
|
21
21
|
throw new Error(`Input HTML file does not exist: ${absoluteHtml}`);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// 确保输出目录存在
|
|
25
24
|
const outputDir = path.dirname(absolutePdf);
|
|
26
25
|
if (!fs.existsSync(outputDir)) {
|
|
27
26
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
@@ -41,8 +40,6 @@ export async function renderResumePdf(options: RenderOptions): Promise<void> {
|
|
|
41
40
|
|
|
42
41
|
const page = await context.newPage();
|
|
43
42
|
await page.goto(`file://${absoluteHtml}`, { waitUntil: 'networkidle' });
|
|
44
|
-
|
|
45
|
-
// 确保所有 Web 字体准备就绪
|
|
46
43
|
await page.evaluateHandle('document.fonts.ready');
|
|
47
44
|
|
|
48
45
|
await page.pdf({
|
|
@@ -69,6 +66,9 @@ async function main() {
|
|
|
69
66
|
} else if (args[i] === '--output' || args[i] === '-o') {
|
|
70
67
|
outputPdf = args[i + 1];
|
|
71
68
|
i++;
|
|
69
|
+
} else if (!args[i].startsWith('-')) {
|
|
70
|
+
if (i === 0) inputHtml = args[i];
|
|
71
|
+
else if (i === 1) outputPdf = args[i];
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
KnowMe CareerForge — Resume Workspace Instantiator (Intermediate Working Canvas)
|
|
4
|
+
将指定模板、候选人结构化事实档案 (evidence-master.json) 与 JD 关键词高亮组装为自包含的单文件 HTML 工作区修改场。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys, os, json, argparse, re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, Optional, List
|
|
10
|
+
|
|
11
|
+
def get_project_root() -> Path:
|
|
12
|
+
curr = Path(__file__).resolve().parent
|
|
13
|
+
for _ in range(5):
|
|
14
|
+
if (curr / "SKILL.md").exists() or (curr / "package.json").exists() or (curr / "src" / "templates").exists():
|
|
15
|
+
return curr
|
|
16
|
+
curr = curr.parent
|
|
17
|
+
return Path.cwd()
|
|
18
|
+
|
|
19
|
+
def highlight_keywords(text: str, keywords: List[str]) -> str:
|
|
20
|
+
if not keywords or not text:
|
|
21
|
+
return text
|
|
22
|
+
for kw in keywords:
|
|
23
|
+
kw_clean = kw.strip()
|
|
24
|
+
if kw_clean:
|
|
25
|
+
pattern = re.compile(rf'(?<!<strong>)(?<![<\w/])({re.escape(kw_clean)})(?![>\w])(?!</strong>)', re.IGNORECASE)
|
|
26
|
+
text = pattern.sub(r"<strong>\1</strong>", text)
|
|
27
|
+
return text
|
|
28
|
+
|
|
29
|
+
def render_profile_into_html(html_str: str, profile: Dict[str, Any], keywords: Optional[List[str]] = None) -> str:
|
|
30
|
+
basics = profile.get("basics", {})
|
|
31
|
+
name = basics.get("name", "")
|
|
32
|
+
title = basics.get("title", "")
|
|
33
|
+
phone = basics.get("phone", "")
|
|
34
|
+
email = basics.get("email", "")
|
|
35
|
+
location = basics.get("location", "")
|
|
36
|
+
github = basics.get("github", "")
|
|
37
|
+
|
|
38
|
+
# 1. 替换基础信息
|
|
39
|
+
if name:
|
|
40
|
+
html_str = re.sub(r'(<h1[^>]*class="[^"]*candidate-name[^"]*"[^>]*>).*?(</h1>)', rf'\g<1>{name}\g<2>', html_str)
|
|
41
|
+
html_str = re.sub(r'(<title>).*?(</title>)', f'<title>{name} - 个人简历</title>', html_str)
|
|
42
|
+
|
|
43
|
+
if title:
|
|
44
|
+
html_str = re.sub(r'(<(?:p|span)[^>]*class="[^"]*(?:job-target|target-title)[^"]*"[^>]*>).*?(</(?:p|span)>)', rf'\g<1>求职意向:{title}\g<2>', html_str)
|
|
45
|
+
|
|
46
|
+
if phone:
|
|
47
|
+
html_str = re.sub(r'138-0000-0000|\b1[3-9]\d{9}\b|\+?\d{1,4}[-\s]?\d{7,11}', phone, html_str)
|
|
48
|
+
|
|
49
|
+
if email:
|
|
50
|
+
html_str = re.sub(r'[a-zA-Z0-9_.+-]+@example\.com|[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+', email, html_str)
|
|
51
|
+
|
|
52
|
+
if location:
|
|
53
|
+
html_str = re.sub(r'北京\s*[·•-]\s*海淀(?:区)?', location, html_str)
|
|
54
|
+
|
|
55
|
+
if github:
|
|
56
|
+
html_str = re.sub(r'github\.com/[a-zA-Z0-9_-]+', github.replace("https://", "").replace("http://", ""), html_str)
|
|
57
|
+
|
|
58
|
+
# 2. 注入经历
|
|
59
|
+
experiences = profile.get("experience", [])
|
|
60
|
+
if experiences and '<section class="resume-section" id="experience">' in html_str:
|
|
61
|
+
exp_html_items = []
|
|
62
|
+
for exp in experiences:
|
|
63
|
+
bullets_html = "".join([f"<li>{highlight_keywords(b.get('text', ''), keywords or [])}</li>" for b in exp.get("bullets", [])])
|
|
64
|
+
exp_html_items.append(f"""
|
|
65
|
+
<div class="experience-item">
|
|
66
|
+
<div class="item-header">
|
|
67
|
+
<div class="item-title-group">
|
|
68
|
+
<span class="org-name">{exp.get('company', '')}</span>
|
|
69
|
+
<span class="role-badge">{exp.get('role', '')}</span>
|
|
70
|
+
</div>
|
|
71
|
+
<span class="date-range">{exp.get('startDate', '')} - {exp.get('endDate', '')}</span>
|
|
72
|
+
</div>
|
|
73
|
+
<ul class="bullet-list">
|
|
74
|
+
{bullets_html}
|
|
75
|
+
</ul>
|
|
76
|
+
</div>""")
|
|
77
|
+
|
|
78
|
+
exp_block_pattern = re.compile(r'(<section[^>]*id="experience"[^>]*>.*?<h2[^>]*>.*?</h2>\s*).*?(</section>)', re.DOTALL)
|
|
79
|
+
if exp_block_pattern.search(html_str):
|
|
80
|
+
html_str = exp_block_pattern.sub(rf'\g<1>{"".join(exp_html_items)}\n \g<2>', html_str)
|
|
81
|
+
|
|
82
|
+
# 3. 注入项目
|
|
83
|
+
projects = profile.get("projects", [])
|
|
84
|
+
if projects and '<section class="resume-section" id="projects">' in html_str:
|
|
85
|
+
proj_html_items = []
|
|
86
|
+
for proj in projects:
|
|
87
|
+
bullets_html = "".join([f"<li>{highlight_keywords(b.get('text', ''), keywords or [])}</li>" for b in proj.get("bullets", [])])
|
|
88
|
+
tech_tags_html = "".join([f'<span class="tech-tag">{t}</span>' for t in proj.get("techStack", [])])
|
|
89
|
+
proj_html_items.append(f"""
|
|
90
|
+
<div class="project-item">
|
|
91
|
+
<div class="item-header">
|
|
92
|
+
<div class="item-title-group">
|
|
93
|
+
<span class="project-name">{proj.get('name', '')}</span>
|
|
94
|
+
<span class="role-badge">{proj.get('role', '')}</span>
|
|
95
|
+
</div>
|
|
96
|
+
<span class="date-range">{proj.get('startDate', '')} - {proj.get('endDate', '')}</span>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="tech-stack-tags">
|
|
99
|
+
{tech_tags_html}
|
|
100
|
+
</div>
|
|
101
|
+
<ul class="bullet-list">
|
|
102
|
+
{bullets_html}
|
|
103
|
+
</ul>
|
|
104
|
+
</div>""")
|
|
105
|
+
|
|
106
|
+
proj_block_pattern = re.compile(r'(<section[^>]*id="projects"[^>]*>.*?<h2[^>]*>.*?</h2>\s*).*?(</section>)', re.DOTALL)
|
|
107
|
+
if proj_block_pattern.search(html_str):
|
|
108
|
+
html_str = proj_block_pattern.sub(rf'\g<1>{"".join(proj_html_items)}\n \g<2>', html_str)
|
|
109
|
+
|
|
110
|
+
return html_str
|
|
111
|
+
|
|
112
|
+
def instantiate_workspace(template_id: str, profile_path: Optional[str] = None, keywords: Optional[str] = None, output_path: str = "workspace/resume.html") -> Path:
|
|
113
|
+
base_dir = get_project_root()
|
|
114
|
+
template_dir = base_dir / "src" / "templates" / template_id
|
|
115
|
+
|
|
116
|
+
if not template_dir.exists():
|
|
117
|
+
for sub in (base_dir / "src" / "templates").iterdir():
|
|
118
|
+
if sub.is_dir() and (sub / "metadata.json").exists():
|
|
119
|
+
try:
|
|
120
|
+
meta = json.loads((sub / "metadata.json").read_text(encoding="utf-8"))
|
|
121
|
+
if meta.get("id") == template_id:
|
|
122
|
+
template_dir = sub
|
|
123
|
+
break
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
if not template_dir.exists():
|
|
128
|
+
raise ValueError(f"Template '{template_id}' not found in src/templates/")
|
|
129
|
+
|
|
130
|
+
html_file = template_dir / "template.html"
|
|
131
|
+
css_file = template_dir / "style.css"
|
|
132
|
+
|
|
133
|
+
if not html_file.exists() or not css_file.exists():
|
|
134
|
+
raise FileNotFoundError(f"template.html or style.css missing in {template_dir}")
|
|
135
|
+
|
|
136
|
+
html_content = html_file.read_text(encoding="utf-8")
|
|
137
|
+
css_content = css_file.read_text(encoding="utf-8")
|
|
138
|
+
|
|
139
|
+
inlined_html = html_content.replace(
|
|
140
|
+
'<link rel="stylesheet" href="style.css">',
|
|
141
|
+
f'<style>\n{css_content}\n </style>'
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
kw_list = [k.strip() for k in keywords.split(",") if k.strip()] if keywords else []
|
|
145
|
+
|
|
146
|
+
if profile_path and Path(profile_path).exists():
|
|
147
|
+
try:
|
|
148
|
+
profile_data = json.loads(Path(profile_path).read_text(encoding="utf-8"))
|
|
149
|
+
inlined_html = render_profile_into_html(inlined_html, profile_data, kw_list)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
print(f"[!] Warning: Failed to inject profile data: {e}")
|
|
152
|
+
elif kw_list:
|
|
153
|
+
inlined_html = highlight_keywords(inlined_html, kw_list)
|
|
154
|
+
|
|
155
|
+
out_file = Path(output_path)
|
|
156
|
+
out_file.parent.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
out_file.write_text(inlined_html, encoding="utf-8")
|
|
158
|
+
|
|
159
|
+
print("==============================================================")
|
|
160
|
+
print(f" KnowMe CareerForge — Intermediate Canvas Instantiated!")
|
|
161
|
+
print(f" Selected Template : {template_id} ({template_dir.name})")
|
|
162
|
+
print(f" Profile Injected : {profile_path or 'Default Template Content'}")
|
|
163
|
+
print(f" Working Canvas : {out_file.resolve()}")
|
|
164
|
+
print("==============================================================")
|
|
165
|
+
return out_file
|
|
166
|
+
|
|
167
|
+
def main():
|
|
168
|
+
parser = argparse.ArgumentParser(description="KnowMe CareerForge — Workspace Instantiator")
|
|
169
|
+
parser.add_argument("--template", "-t", required=True, help="Template ID (minimal, modern, executive, classic)")
|
|
170
|
+
parser.add_argument("--profile", "-p", default=None, help="Path to evidence-master.json candidate profile")
|
|
171
|
+
parser.add_argument("--keywords", "-k", default=None, help="Comma-separated keywords to highlight (e.g. 'Python,LLM,RAG')")
|
|
172
|
+
parser.add_argument("--output", "-o", default="workspace/resume.html", help="Output canvas path (default: workspace/resume.html)")
|
|
173
|
+
|
|
174
|
+
args = parser.parse_args()
|
|
175
|
+
instantiate_workspace(args.template, profile_path=args.profile, keywords=args.keywords, output_path=args.output)
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
main()
|
|
@@ -6,9 +6,19 @@ KnowMe CareerForge — Template Search & Ranking Engine (BM25 + Multi-criteria s
|
|
|
6
6
|
|
|
7
7
|
import sys, os, json, argparse
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
from typing import List, Dict, Any, Optional
|
|
10
|
+
|
|
11
|
+
def get_project_root() -> Path:
|
|
12
|
+
curr = Path(__file__).resolve().parent
|
|
13
|
+
for _ in range(5):
|
|
14
|
+
if (curr / "SKILL.md").exists() or (curr / "package.json").exists() or (curr / "src" / "templates").exists():
|
|
15
|
+
return curr
|
|
16
|
+
curr = curr.parent
|
|
17
|
+
return Path.cwd()
|
|
18
|
+
|
|
19
|
+
def load_templates() -> List[Dict[str, Any]]:
|
|
20
|
+
root_dir = get_project_root()
|
|
21
|
+
base_dir = root_dir / "src" / "templates"
|
|
12
22
|
templates = []
|
|
13
23
|
if not base_dir.exists():
|
|
14
24
|
return templates
|
|
@@ -22,11 +32,11 @@ def load_templates():
|
|
|
22
32
|
data = json.loads(meta_file.read_text(encoding="utf-8"))
|
|
23
33
|
data["directory"] = f"src/templates/{t_dir.name}"
|
|
24
34
|
templates.append(data)
|
|
25
|
-
except Exception
|
|
35
|
+
except Exception:
|
|
26
36
|
pass
|
|
27
37
|
return templates
|
|
28
38
|
|
|
29
|
-
def calculate_match_score(template, query_role, query_style=None, target_pages=1, density="balanced"):
|
|
39
|
+
def calculate_match_score(template: Dict[str, Any], query_role: str, query_style: Optional[str] = None, target_pages: int = 1, density: str = "balanced") -> float:
|
|
30
40
|
score = 0.0
|
|
31
41
|
|
|
32
42
|
# 1. 岗位匹配度 (Role Match - 35%)
|
|
@@ -41,10 +51,12 @@ def calculate_match_score(template, query_role, query_style=None, target_pages=1
|
|
|
41
51
|
|
|
42
52
|
if role_score < 0.5:
|
|
43
53
|
cat = template.get("roleCategory", "")
|
|
44
|
-
if "
|
|
45
|
-
role_score = 0.
|
|
46
|
-
elif "
|
|
47
|
-
role_score = 0.
|
|
54
|
+
if "ai" in query_lower or "algorithm" in query_lower or "agent" in query_lower:
|
|
55
|
+
role_score = 0.95 if cat == "engineering-ai" else 0.40
|
|
56
|
+
elif "product" in query_lower or "manager" in query_lower or "director" in query_lower:
|
|
57
|
+
role_score = 0.95 if cat == "management-product" else 0.40
|
|
58
|
+
elif "engineer" in query_lower or "architect" in query_lower:
|
|
59
|
+
role_score = 0.85
|
|
48
60
|
else:
|
|
49
61
|
role_score = 0.60
|
|
50
62
|
|
|
@@ -55,15 +67,16 @@ def calculate_match_score(template, query_role, query_style=None, target_pages=1
|
|
|
55
67
|
if query_style:
|
|
56
68
|
q_style_lower = query_style.lower()
|
|
57
69
|
t_style = template.get("style", "").lower()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
elif "single" in q_style_lower and "single" in t_style:
|
|
70
|
+
t_tone = template.get("visualStyle", {}).get("tone", "").lower()
|
|
71
|
+
|
|
72
|
+
if q_style_lower in t_style or q_style_lower in t_tone or q_style_lower == template.get("id"):
|
|
62
73
|
style_score = 1.0
|
|
63
|
-
elif "
|
|
64
|
-
style_score =
|
|
65
|
-
elif "
|
|
66
|
-
style_score =
|
|
74
|
+
elif "minimal" in q_style_lower and "minimal" in t_style:
|
|
75
|
+
style_score = 0.95
|
|
76
|
+
elif "split" in q_style_lower and "split" in t_style:
|
|
77
|
+
style_score = 0.95
|
|
78
|
+
elif "modern" in q_style_lower and "modern" in t_tone:
|
|
79
|
+
style_score = 0.95
|
|
67
80
|
else:
|
|
68
81
|
style_score = 0.50
|
|
69
82
|
score += style_score * 0.25
|
|
@@ -91,12 +104,10 @@ def calculate_match_score(template, query_role, query_style=None, target_pages=1
|
|
|
91
104
|
|
|
92
105
|
return round(score * 100, 1)
|
|
93
106
|
|
|
94
|
-
def search_templates(role, style=None, target_pages=1, density="balanced"):
|
|
107
|
+
def search_templates(role: str, style: Optional[str] = None, target_pages: int = 1, density: str = "balanced") -> List[Dict[str, Any]]:
|
|
95
108
|
templates = load_templates()
|
|
96
|
-
if not templates:
|
|
97
|
-
return []
|
|
98
|
-
|
|
99
109
|
results = []
|
|
110
|
+
|
|
100
111
|
for t in templates:
|
|
101
112
|
s = calculate_match_score(t, role, style, target_pages, density)
|
|
102
113
|
results.append({
|
|
@@ -109,38 +120,29 @@ def search_templates(role, style=None, target_pages=1, density="balanced"):
|
|
|
109
120
|
|
|
110
121
|
def main():
|
|
111
122
|
parser = argparse.ArgumentParser(description="KnowMe CareerForge — Template Search Engine")
|
|
112
|
-
parser.add_argument("role", nargs="?", default="AI Agent Engineer", help="Target role title
|
|
113
|
-
parser.add_argument("--style", default=None, help="
|
|
114
|
-
parser.add_argument("--target-pages", type=int, default=1, help="Expected
|
|
115
|
-
parser.add_argument("--density",
|
|
116
|
-
parser.add_argument("--json", action="store_true", help="Output results in JSON
|
|
123
|
+
parser.add_argument("role", nargs="?", default="AI Agent Engineer", help="Target role title")
|
|
124
|
+
parser.add_argument("--style", "-s", default=None, help="Desired style (minimal, modern, executive, classic)")
|
|
125
|
+
parser.add_argument("--target-pages", "-p", type=int, default=1, help="Expected target pages (1 or 2)")
|
|
126
|
+
parser.add_argument("--density", "-d", default="balanced", help="Information density preference (high, balanced, normal)")
|
|
127
|
+
parser.add_argument("--json", action="store_true", help="Output results in raw JSON")
|
|
117
128
|
|
|
118
129
|
args = parser.parse_args()
|
|
119
|
-
|
|
120
|
-
results = search_templates(args.role, args.style, args.target_pages, args.density)
|
|
130
|
+
results = search_templates(args.role, style=args.style, target_pages=args.target_pages, density=args.density)
|
|
121
131
|
|
|
122
132
|
if args.json:
|
|
123
133
|
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
124
134
|
return
|
|
125
135
|
|
|
126
136
|
print("=" * 80)
|
|
127
|
-
print(f" KnowMe CareerForge — Template
|
|
128
|
-
print(f" Target Role : {args.role}")
|
|
129
|
-
print(f" Style Pref : {args.style or 'Any'}")
|
|
130
|
-
print(f" Target Pages: {args.target_pages} | Density: {args.density}")
|
|
137
|
+
print(f" KnowMe CareerForge — Template Recommendations for '{args.role}'")
|
|
131
138
|
print("=" * 80)
|
|
132
|
-
|
|
133
|
-
if not results:
|
|
134
|
-
print("No matching templates found.")
|
|
135
|
-
return
|
|
136
|
-
|
|
137
139
|
for idx, r in enumerate(results, 1):
|
|
138
140
|
t = r["template"]
|
|
139
|
-
print(f"{idx}. [Score: {r['score']}] {t
|
|
140
|
-
print(f" Category: {t
|
|
141
|
-
print(f" Target Pages: {t
|
|
142
|
-
print(f"
|
|
143
|
-
print(f" Directory: {t
|
|
141
|
+
print(f"{idx}. [Score: {r['score']}] {t['name']} (ID: {t['id']})")
|
|
142
|
+
print(f" Category: {t['roleCategory']} | Style: {t['style']} | ATS Tier: {t['atsScoreTier']}")
|
|
143
|
+
print(f" Target Pages: {t['layout']['targetPages']} (Max: {t['layout']['maxPages']}) | Density: {t['layout']['density']}")
|
|
144
|
+
print(f" Tone: {t['visualStyle']['tone']} | Accent: {t['visualStyle']['accentColor']}")
|
|
145
|
+
print(f" Directory: {t['directory']}")
|
|
144
146
|
print("-" * 80)
|
|
145
147
|
|
|
146
148
|
if __name__ == "__main__":
|