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.
Files changed (47) hide show
  1. package/README.md +6 -9
  2. package/README.zh-CN.md +6 -9
  3. package/SKILL.md +95 -116
  4. package/agents/claude/knowme-careerforge.md +9 -8
  5. package/agents/codex/knowme-careerforge.yaml +15 -6
  6. package/agents/cursor/knowme-careerforge.mdc +7 -6
  7. package/agents/opencode/skill.yaml +1 -1
  8. package/agents/windsurf/knowme-careerforge.rules +11 -8
  9. package/dist/cli/src/commands/init.d.ts +1 -1
  10. package/dist/cli/src/commands/init.js +37 -10
  11. package/dist/cli/src/index.js +65 -22
  12. package/dist/scripts/render-pdf.d.ts +0 -9
  13. package/dist/scripts/render-pdf.js +5 -68
  14. package/dist/scripts/rendering/render-pdf.d.ts +11 -0
  15. package/dist/scripts/rendering/render-pdf.js +109 -0
  16. package/dist/scripts/validate-ats.d.ts +0 -21
  17. package/dist/scripts/validate-ats.js +5 -132
  18. package/dist/scripts/validate-layout.d.ts +0 -22
  19. package/dist/scripts/validate-layout.js +5 -124
  20. package/dist/scripts/validation/validate-ats.d.ts +23 -0
  21. package/dist/scripts/validation/validate-ats.js +173 -0
  22. package/dist/scripts/validation/validate-layout.d.ts +24 -0
  23. package/dist/scripts/validation/validate-layout.js +164 -0
  24. package/package.json +15 -12
  25. package/scripts/Agent.md +138 -0
  26. package/scripts/{build-gallery.py → build/build-gallery.py} +128 -99
  27. package/scripts/{build-knowledge.py → build/build-knowledge.py} +9 -1
  28. package/scripts/{release.py → build/release.py} +20 -16
  29. package/scripts/{run-all-tests.py → build/run-all-tests.py} +9 -1
  30. package/scripts/build/sync-version.py +107 -0
  31. package/scripts/{analyze-jd.py → evidence/analyze-jd.py} +34 -45
  32. package/scripts/evidence/extract-evidence.py +348 -0
  33. package/scripts/pipeline/forge.py +142 -0
  34. package/scripts/rendering/render-pdf.py +162 -0
  35. package/scripts/{render-pdf.ts → rendering/render-pdf.ts} +3 -3
  36. package/scripts/template/instantiate-resume.py +178 -0
  37. package/scripts/{search-template.py → template/search-template.py} +44 -42
  38. package/scripts/{validate-ats.ts → validation/validate-ats.ts} +39 -35
  39. package/scripts/{validate-layout.ts → validation/validate-layout.ts} +43 -42
  40. package/scripts/{validate-resume.py → validation/validate-resume.py} +31 -22
  41. package/skill.json +1 -1
  42. package/src/knowledge/index.json +1 -1
  43. package/src/knowledge/resume-schema.json +112 -0
  44. package/src/workflows/forge-resume.md +2 -2
  45. package/src/workflows/review.md +3 -3
  46. package/scripts/instantiate-resume.py +0 -83
  47. 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
- def load_templates():
11
- base_dir = Path(__file__).resolve().parent.parent / "src" / "templates"
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 as e:
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 "tech" in query_lower or "engineer" in query_lower or "ai" in query_lower or "研发" in query_lower:
45
- role_score = 0.90 if cat == "engineering-ai" else 0.40
46
- elif "manage" in query_lower or "lead" in query_lower or "director" in query_lower or "总监" in query_lower or "架构" in query_lower or "产品" in query_lower:
47
- role_score = 0.90 if cat == "management-product" else 0.50
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
- t_id = template.get("id", "").lower()
59
- if q_style_lower in t_style or q_style_lower in t_id:
60
- style_score = 1.0
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 "split" in q_style_lower or "double" in q_style_lower or "column" in q_style_lower:
64
- style_score = 1.0 if "two-column" in t_style else 0.50
65
- elif "table" in q_style_lower or "grid" in q_style_lower:
66
- style_score = 1.0 if "table" in t_style else 0.40
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 (e.g. 'AI Agent Engineer', 'Frontend')")
113
- parser.add_argument("--style", default=None, help="Style filter: minimal, modern, executive, classic, single-column, two-column, table")
114
- parser.add_argument("--target-pages", type=int, default=1, help="Expected page count (1 or 2)")
115
- parser.add_argument("--density", default="balanced", choices=["high", "balanced", "relaxed"], help="Information density")
116
- parser.add_argument("--json", action="store_true", help="Output results in JSON format")
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 Search Results")
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.get('name')} (ID: {t.get('id')})")
140
- print(f" Category: {t.get('roleCategory')} | Style: {t.get('style')} | ATS Tier: {t.get('atsScoreTier')}")
141
- print(f" Target Pages: {t.get('layout', {}).get('targetPages')} (Max: {t.get('layout', {}).get('maxPages')}) | Density: {t.get('layout', {}).get('density')}")
142
- print(f" Customizable Tokens: {', '.join(t.get('customizableTokens', [])[:4])}")
143
- print(f" Directory: {t.get('directory')}")
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__":