knowme-careerforge-skill 0.0.2 → 0.0.6
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/AGENT.md +276 -0
- package/ARCHITECTURE.md +216 -0
- package/README.md +34 -54
- package/README.zh-CN.md +35 -57
- package/SKILL.md +28 -137
- package/agents/claude/knowme-careerforge.md +2 -16
- package/agents/codex/knowme-careerforge.yaml +5 -7
- package/agents/cursor/knowme-careerforge.mdc +4 -9
- package/agents/opencode/skill.yaml +1 -1
- package/agents/windsurf/knowme-careerforge.rules +2 -12
- package/dist/cli/src/commands/init.js +9 -0
- package/dist/cli/src/index.js +40 -19
- package/dist/scripts/rendering/engine-loader.d.ts +2 -0
- package/dist/scripts/rendering/engine-loader.js +46 -0
- package/dist/scripts/rendering/render-pdf.d.ts +3 -8
- package/dist/scripts/rendering/render-pdf.js +7 -103
- package/dist/scripts/rendering/runtime.d.ts +9 -0
- package/dist/scripts/rendering/runtime.js +63 -0
- package/dist/scripts/validation/validate-ats.d.ts +2 -22
- package/dist/scripts/validation/validate-ats.js +6 -168
- package/dist/scripts/validation/validate-layout.d.ts +2 -23
- package/dist/scripts/validation/validate-layout.js +5 -159
- package/package.json +16 -4
- package/references/01-evidence-mining.md +5 -0
- package/references/02-career-goal.md +38 -0
- package/references/03-jd-analysis.md +44 -0
- package/references/04-template-selection.md +43 -0
- package/references/05-html-canvas-tokens.md +12 -0
- package/references/06-qa-and-rendering.md +14 -0
- package/references/07-artifact-contract.md +64 -0
- package/scripts/Agent.md +9 -136
- package/scripts/build/build-gallery.py +66 -56
- package/scripts/build/run-all-tests.py +1 -0
- package/scripts/build/sync-version.py +37 -10
- package/scripts/contracts/profile.py +101 -0
- package/scripts/evidence/extract-evidence.py +109 -62
- package/scripts/pipeline/forge.py +266 -136
- package/scripts/rendering/README.md +65 -0
- package/scripts/rendering/browser-engine.d.ts +15 -0
- package/scripts/rendering/browser-engine.js +366 -0
- package/scripts/rendering/browser_engine.py +39 -0
- package/scripts/rendering/engine-loader.ts +11 -0
- package/scripts/rendering/render-pdf.py +26 -151
- package/scripts/rendering/render-pdf.ts +7 -83
- package/scripts/template/instantiate-resume.py +266 -167
- package/scripts/template/search-template.py +367 -89
- package/scripts/validation/measure-dom.js +5 -0
- package/scripts/validation/validate-ats.ts +6 -174
- package/scripts/validation/validate-layout.ts +5 -159
- package/scripts/validation/validate-resume.py +23 -134
- package/skill.json +1 -1
- package/src/knowledge/index.json +390 -10
- package/src/knowledge/resume-schema.json +377 -71
- package/src/knowledge/templates.json +388 -8
- package/src/references/01-evidence-mining.md +5 -0
- package/src/references/02-career-goal.md +38 -0
- package/src/references/03-jd-analysis.md +44 -0
- package/src/references/04-template-selection.md +43 -0
- package/src/references/05-html-canvas-tokens.md +12 -0
- package/src/references/06-qa-and-rendering.md +14 -0
- package/src/references/07-artifact-contract.md +64 -0
- package/src/templates/Agent.md +11 -0
- package/src/templates/academic-research/README.md +25 -0
- package/src/templates/academic-research/canvas.html +21 -0
- package/src/templates/academic-research/metadata.json +38 -0
- package/src/templates/academic-research/sample-profile.json +86 -0
- package/src/templates/academic-research/style.css +213 -0
- package/src/templates/classic/README.md +9 -0
- package/src/templates/classic/canvas.html +21 -0
- package/src/templates/classic/metadata.json +3 -0
- package/src/templates/classic/sample-profile.json +86 -0
- package/src/templates/classic/style.css +35 -54
- package/src/templates/common/base.css +182 -0
- package/src/templates/common/canvas-bindings.css +41 -0
- package/src/templates/common/resume-contract.md +8 -0
- package/src/templates/compact-dense/README.md +25 -0
- package/src/templates/compact-dense/canvas.html +14 -0
- package/src/templates/compact-dense/metadata.json +38 -0
- package/src/templates/compact-dense/sample-profile.json +86 -0
- package/src/templates/compact-dense/style.css +218 -0
- package/src/templates/creative-tech/README.md +26 -0
- package/src/templates/creative-tech/canvas.html +15 -0
- package/src/templates/creative-tech/metadata.json +39 -0
- package/src/templates/creative-tech/sample-profile.json +86 -0
- package/src/templates/creative-tech/style.css +257 -0
- package/src/templates/data-analyst/README.md +26 -0
- package/src/templates/data-analyst/canvas.html +15 -0
- package/src/templates/data-analyst/metadata.json +39 -0
- package/src/templates/data-analyst/sample-profile.json +86 -0
- package/src/templates/data-analyst/style.css +285 -0
- package/src/templates/executive/README.md +9 -0
- package/src/templates/executive/canvas.html +15 -0
- package/src/templates/executive/sample-profile.json +86 -0
- package/src/templates/executive/style.css +83 -74
- package/src/templates/international-flow/README.md +26 -0
- package/src/templates/international-flow/canvas.html +21 -0
- package/src/templates/international-flow/metadata.json +37 -0
- package/src/templates/international-flow/sample-profile.json +86 -0
- package/src/templates/international-flow/style.css +208 -0
- package/src/templates/minimal/README.md +9 -0
- package/src/templates/minimal/canvas.html +21 -0
- package/src/templates/minimal/sample-profile.json +86 -0
- package/src/templates/minimal/style.css +54 -72
- package/src/templates/modern/README.md +9 -0
- package/src/templates/modern/canvas.html +15 -0
- package/src/templates/modern/sample-profile.json +86 -0
- package/src/templates/modern/style.css +78 -71
- package/src/templates/startup-generalist/README.md +25 -0
- package/src/templates/startup-generalist/canvas.html +21 -0
- package/src/templates/startup-generalist/metadata.json +37 -0
- package/src/templates/startup-generalist/sample-profile.json +86 -0
- package/src/templates/startup-generalist/style.css +237 -0
- package/src/references/ats.md +0 -17
- package/src/references/evidence-first.md +0 -22
- package/src/references/pdf.md +0 -40
- package/src/references/resume-writing.md +0 -25
- package/src/references/template-design.md +0 -57
- package/src/templates/classic/template.html +0 -133
- package/src/templates/executive/template.html +0 -101
- package/src/templates/minimal/template.html +0 -114
- package/src/templates/modern/template.html +0 -139
- package/src/workflows/analyze-job.md +0 -16
- package/src/workflows/define-goal.md +0 -16
- package/src/workflows/forge-resume.md +0 -25
- package/src/workflows/know-me.md +0 -21
- package/src/workflows/position.md +0 -19
- package/src/workflows/review.md +0 -31
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
KnowMe CareerForge — Template Search & Ranking Engine
|
|
4
|
-
|
|
3
|
+
KnowMe CareerForge — Template Search & Ranking Engine
|
|
4
|
+
Architecture: Deep Module with Pluggable Scorer Strategies (Weighted Rules + BM25 Text Search)
|
|
5
|
+
|
|
6
|
+
Design Contract:
|
|
7
|
+
- BaseTemplateScorer: Abstract strategy interface
|
|
8
|
+
- WeightedRuleScorer: Multi-criteria rule-based weighted scorer (Role 35%, Style 25%, ATS 20%, Page 10%, Density 10%)
|
|
9
|
+
- BM25TextScorer: Pure-Python BM25 keyword retrieval across template text documents
|
|
10
|
+
- HybridTemplateScorer: Weighted blend (70% Rule Score + 30% BM25 Text Score)
|
|
11
|
+
- search_templates(): Public facade interface
|
|
5
12
|
"""
|
|
6
13
|
|
|
7
|
-
import sys, os, json, argparse
|
|
14
|
+
import sys, os, json, argparse, math, re
|
|
8
15
|
from pathlib import Path
|
|
16
|
+
from dataclasses import dataclass, field
|
|
9
17
|
from typing import List, Dict, Any, Optional
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
|
|
10
20
|
|
|
11
21
|
def get_project_root() -> Path:
|
|
12
22
|
curr = Path(__file__).resolve().parent
|
|
@@ -16,6 +26,17 @@ def get_project_root() -> Path:
|
|
|
16
26
|
curr = curr.parent
|
|
17
27
|
return Path.cwd()
|
|
18
28
|
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class SearchQuery:
|
|
32
|
+
role: str
|
|
33
|
+
style: Optional[str] = None
|
|
34
|
+
target_pages: int = 1
|
|
35
|
+
density: str = "balanced"
|
|
36
|
+
keywords: Optional[str] = None
|
|
37
|
+
engine: str = "hybrid" # "weighted" | "bm25" | "hybrid"
|
|
38
|
+
|
|
39
|
+
|
|
19
40
|
def load_templates() -> List[Dict[str, Any]]:
|
|
20
41
|
root_dir = get_project_root()
|
|
21
42
|
base_dir = root_dir / "src" / "templates"
|
|
@@ -30,120 +51,377 @@ def load_templates() -> List[Dict[str, Any]]:
|
|
|
30
51
|
if meta_file.exists():
|
|
31
52
|
try:
|
|
32
53
|
data = json.loads(meta_file.read_text(encoding="utf-8"))
|
|
33
|
-
data["
|
|
54
|
+
data["_directory"] = str(t_dir.relative_to(root_dir))
|
|
34
55
|
templates.append(data)
|
|
35
56
|
except Exception:
|
|
36
57
|
pass
|
|
37
58
|
return templates
|
|
38
59
|
|
|
39
|
-
|
|
40
|
-
|
|
60
|
+
|
|
61
|
+
# ==============================================================================
|
|
62
|
+
# 1. SCORER STRATEGY INTERFACE & IMPLEMENTATIONS
|
|
63
|
+
# ==============================================================================
|
|
64
|
+
|
|
65
|
+
class BaseTemplateScorer(ABC):
|
|
66
|
+
"""Abstract interface for template scoring algorithms."""
|
|
41
67
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def score(self, template: Dict[str, Any], query: SearchQuery) -> float:
|
|
70
|
+
"""Return match score normalized between 0.0 and 100.0."""
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class WeightedRuleScorer(BaseTemplateScorer):
|
|
75
|
+
"""
|
|
76
|
+
Rule-based weighted scoring engine:
|
|
77
|
+
- Role Match: 35%
|
|
78
|
+
- Style Match: 25%
|
|
79
|
+
- ATS Tier: 20%
|
|
80
|
+
- Page Fit: 10%
|
|
81
|
+
- Density Fit: 10%
|
|
82
|
+
"""
|
|
46
83
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
score += role_score * 0.35
|
|
64
|
-
|
|
65
|
-
# 2. 风格匹配度 (Style Match - 25%)
|
|
66
|
-
style_score = 0.70
|
|
67
|
-
if query_style:
|
|
68
|
-
q_style_lower = query_style.lower()
|
|
69
|
-
t_style = template.get("style", "").lower()
|
|
70
|
-
t_tone = template.get("visualStyle", {}).get("tone", "").lower()
|
|
84
|
+
def __init__(self, weights: Optional[Dict[str, float]] = None):
|
|
85
|
+
self.weights = weights or {
|
|
86
|
+
"role": 0.35,
|
|
87
|
+
"style": 0.25,
|
|
88
|
+
"ats": 0.20,
|
|
89
|
+
"page": 0.10,
|
|
90
|
+
"density": 0.10
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def score(self, template: Dict[str, Any], query: SearchQuery) -> float:
|
|
94
|
+
score = 0.0
|
|
95
|
+
|
|
96
|
+
# 1. Role Match
|
|
97
|
+
role_score = 0.0
|
|
98
|
+
query_lower = query.role.lower()
|
|
99
|
+
supported = [r.lower() for r in template.get("supportedRoles", [])]
|
|
71
100
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
101
|
+
for r in supported:
|
|
102
|
+
if r in query_lower or query_lower in r:
|
|
103
|
+
role_score = 1.0
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
if role_score < 0.5:
|
|
107
|
+
cat = template.get("roleCategory", "")
|
|
108
|
+
if any(k in query_lower for k in ["research", "scientist", "algorithm", "phd", "academic", "postdoc", "算法", "科研", "学者", "研究员"]) and "research" in cat:
|
|
109
|
+
role_score = 0.90
|
|
110
|
+
elif any(k in query_lower for k in ["remote", "international", "global", "overseas", "foreign", "外企", "海外", "跨国", "远程"]) and "international" in cat:
|
|
111
|
+
role_score = 0.90
|
|
112
|
+
elif any(k in query_lower for k in ["creative", "ui", "ux", "design", "interaction", "动效", "交互", "视觉"]) and "creative" in cat:
|
|
113
|
+
role_score = 0.90
|
|
114
|
+
elif any(k in query_lower for k in ["data", "analyst", "bi", "analytics", "metric", "数据", "商业智能", "数仓"]) and "data" in cat:
|
|
115
|
+
role_score = 0.90
|
|
116
|
+
elif any(k in query_lower for k in ["startup", "indie", "growth", "founder", "hacker", "初创", "增长", "独立开发"]) and "startup" in cat:
|
|
117
|
+
role_score = 0.90
|
|
118
|
+
elif any(k in query_lower for k in ["ai", "agent", "llm", "frontend", "backend", "fullstack", "devops", "engineer"]) and "engineering" in cat:
|
|
119
|
+
role_score = 0.85
|
|
120
|
+
elif any(k in query_lower for k in ["manager", "product", "lead", "director", "architect", "cto", "高管", "总监"]) and "management" in cat:
|
|
121
|
+
role_score = 0.85
|
|
122
|
+
elif any(k in query_lower for k in ["bank", "fintech", "state", "corp", "government"]) and "corporate" in cat:
|
|
123
|
+
role_score = 0.85
|
|
124
|
+
else:
|
|
125
|
+
role_score = 0.60
|
|
126
|
+
score += role_score * self.weights["role"]
|
|
127
|
+
|
|
128
|
+
# 2. Style Match
|
|
129
|
+
style_score = 0.70
|
|
130
|
+
if query.style:
|
|
131
|
+
q_style_lower = query.style.lower()
|
|
132
|
+
t_style = template.get("style", "").lower()
|
|
133
|
+
t_tone = template.get("visualStyle", {}).get("tone", "").lower()
|
|
134
|
+
|
|
135
|
+
if q_style_lower in t_style or q_style_lower in t_tone:
|
|
136
|
+
style_score = 1.0
|
|
137
|
+
elif any(k in t_style for k in q_style_lower.split("-")):
|
|
138
|
+
style_score = 0.85
|
|
139
|
+
else:
|
|
140
|
+
style_score = 0.50
|
|
141
|
+
score += style_score * self.weights["style"]
|
|
142
|
+
|
|
143
|
+
# 3. ATS Tier
|
|
144
|
+
ats_tier = template.get("atsScoreTier", "tier-1-optimal")
|
|
145
|
+
ats_score = 1.0 if "optimal" in ats_tier or "tier-1" in ats_tier else 0.85
|
|
146
|
+
score += ats_score * self.weights["ats"]
|
|
147
|
+
|
|
148
|
+
# 4. Page Fit
|
|
149
|
+
target_p = template.get("layout", {}).get("targetPages", 1)
|
|
150
|
+
max_p = template.get("layout", {}).get("maxPages", 2)
|
|
151
|
+
if query.target_pages == target_p:
|
|
152
|
+
page_score = 1.0
|
|
153
|
+
elif query.target_pages <= max_p:
|
|
154
|
+
page_score = 0.80
|
|
80
155
|
else:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
156
|
+
page_score = 0.50
|
|
157
|
+
score += page_score * self.weights["page"]
|
|
158
|
+
|
|
159
|
+
# 5. Density Preference
|
|
160
|
+
t_density = template.get("layout", {}).get("density", "balanced")
|
|
161
|
+
density_score = 1.0 if query.density == t_density else 0.80
|
|
162
|
+
score += density_score * self.weights["density"]
|
|
163
|
+
|
|
164
|
+
return round(score * 100, 1)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class BM25TextScorer(BaseTemplateScorer):
|
|
168
|
+
"""
|
|
169
|
+
Pure Python BM25 Text Scorer across template metadata documents.
|
|
170
|
+
Constructs a virtual document per template: (name, category, style, supportedRoles, tone).
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def __init__(self, templates: Optional[List[Dict[str, Any]]] = None, k1: float = 1.5, b: float = 0.75):
|
|
174
|
+
self.k1 = k1
|
|
175
|
+
self.b = b
|
|
176
|
+
self.templates = templates or load_templates()
|
|
177
|
+
self.docs = [self._extract_doc(t) for t in self.templates]
|
|
178
|
+
self.avg_dl = sum(len(d) for d in self.docs) / max(len(self.docs), 1)
|
|
179
|
+
self.doc_freqs = self._calc_doc_freqs()
|
|
180
|
+
|
|
181
|
+
def _extract_doc(self, template: Dict[str, Any]) -> List[str]:
|
|
182
|
+
text_parts = [
|
|
183
|
+
template.get("name", ""),
|
|
184
|
+
template.get("style", ""),
|
|
185
|
+
template.get("roleCategory", ""),
|
|
186
|
+
template.get("visualStyle", {}).get("tone", ""),
|
|
187
|
+
" ".join(template.get("supportedRoles", []))
|
|
188
|
+
]
|
|
189
|
+
combined = " ".join(text_parts).lower()
|
|
190
|
+
# Tokenize words & Chinese characters
|
|
191
|
+
tokens = re.findall(r'[a-zA-Z0-9_\-]+|[\u4e00-\u9fa5]', combined)
|
|
192
|
+
return tokens
|
|
193
|
+
|
|
194
|
+
def _calc_doc_freqs(self) -> Dict[str, int]:
|
|
195
|
+
freqs: Dict[str, int] = {}
|
|
196
|
+
for doc in self.docs:
|
|
197
|
+
for word in set(doc):
|
|
198
|
+
freqs[word] = freqs.get(word, 0) + 1
|
|
199
|
+
return freqs
|
|
200
|
+
|
|
201
|
+
def score(self, template: Dict[str, Any], query: SearchQuery) -> float:
|
|
202
|
+
doc = self._extract_doc(template)
|
|
203
|
+
doc_len = len(doc)
|
|
204
|
+
if doc_len == 0:
|
|
205
|
+
return 50.0
|
|
206
|
+
|
|
207
|
+
query_text = f"{query.role} {query.style or ''} {query.keywords or ''}".lower()
|
|
208
|
+
q_tokens = re.findall(r'[a-zA-Z0-9_\-]+|[\u4e00-\u9fa5]', query_text)
|
|
209
|
+
if not q_tokens:
|
|
210
|
+
return 70.0
|
|
99
211
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
density_score = 1.0 if density == t_density else 0.80
|
|
103
|
-
score += density_score * 0.10
|
|
212
|
+
n_docs = len(self.docs)
|
|
213
|
+
score = 0.0
|
|
104
214
|
|
|
105
|
-
|
|
215
|
+
for term in q_tokens:
|
|
216
|
+
df = self.doc_freqs.get(term, 0)
|
|
217
|
+
if df == 0:
|
|
218
|
+
continue
|
|
219
|
+
# Standard BM25 IDF with smoothing
|
|
220
|
+
idf = math.log(1 + (n_docs - df + 0.5) / (df + 0.5))
|
|
221
|
+
tf = doc.count(term)
|
|
222
|
+
numerator = tf * (self.k1 + 1)
|
|
223
|
+
denominator = tf + self.k1 * (1 - self.b + self.b * (doc_len / max(self.avg_dl, 1)))
|
|
224
|
+
score += idf * (numerator / max(denominator, 0.001))
|
|
106
225
|
|
|
107
|
-
|
|
226
|
+
# Normalize score into roughly 50.0 ~ 100.0 range
|
|
227
|
+
normalized = 50.0 + min(score * 15.0, 50.0)
|
|
228
|
+
return round(normalized, 1)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class HybridTemplateScorer(BaseTemplateScorer):
|
|
232
|
+
"""
|
|
233
|
+
Weighted combination of rule-based constraints and BM25 text relevance.
|
|
234
|
+
Default: 70% Weighted Rules + 30% BM25 Relevance.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
def __init__(self, rule_weight: float = 0.70, bm25_weight: float = 0.30):
|
|
238
|
+
self.rule_scorer = WeightedRuleScorer()
|
|
239
|
+
self.bm25_scorer = BM25TextScorer()
|
|
240
|
+
self.rule_weight = rule_weight
|
|
241
|
+
self.bm25_weight = bm25_weight
|
|
242
|
+
|
|
243
|
+
def score(self, template: Dict[str, Any], query: SearchQuery) -> float:
|
|
244
|
+
rule_score = self.rule_scorer.score(template, query)
|
|
245
|
+
bm25_score = self.bm25_scorer.score(template, query)
|
|
246
|
+
blended = (rule_score * self.rule_weight) + (bm25_score * self.bm25_weight)
|
|
247
|
+
return round(blended, 1)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
PALETTE_PRESETS: Dict[str, Dict[str, Any]] = {
|
|
251
|
+
"--palette-tech-blue": {
|
|
252
|
+
"name": "经典科技蓝",
|
|
253
|
+
"token": "--palette-tech-blue",
|
|
254
|
+
"hex": "#2563eb",
|
|
255
|
+
"tone": "tech-blue",
|
|
256
|
+
"keywords": ["engineering", "frontend", "backend", "fullstack", "devops", "cloud", "software", "前端", "后端", "全栈"]
|
|
257
|
+
},
|
|
258
|
+
"--palette-deep-navy": {
|
|
259
|
+
"name": "深邃海航蓝",
|
|
260
|
+
"token": "--palette-deep-navy",
|
|
261
|
+
"hex": "#1e3a8a",
|
|
262
|
+
"tone": "oxford-navy",
|
|
263
|
+
"keywords": ["research", "academic", "algorithm", "phd", "scientist", "ai-agent", "llm", "cv", "nlp", "算法", "科研", "学者", "博士"]
|
|
264
|
+
},
|
|
265
|
+
"--palette-teal-modern": {
|
|
266
|
+
"name": "现代松石绿",
|
|
267
|
+
"token": "--palette-teal-modern",
|
|
268
|
+
"hex": "#0f766e",
|
|
269
|
+
"tone": "executive-formal",
|
|
270
|
+
"keywords": ["executive", "management", "cto", "director", "lead", "product", "architect", "高管", "总监", "架构师", "产品经理"]
|
|
271
|
+
},
|
|
272
|
+
"--palette-emerald-fresh": {
|
|
273
|
+
"name": "活力翡翠绿",
|
|
274
|
+
"token": "--palette-emerald-fresh",
|
|
275
|
+
"hex": "#059669",
|
|
276
|
+
"tone": "emerald-green",
|
|
277
|
+
"keywords": ["startup", "generalist", "indie", "growth", "founder", "hacker", "初创", "独立开发", "增长"]
|
|
278
|
+
},
|
|
279
|
+
"--palette-violet-creative": {
|
|
280
|
+
"name": "创意极光紫",
|
|
281
|
+
"token": "--palette-violet-creative",
|
|
282
|
+
"hex": "#7c3aed",
|
|
283
|
+
"tone": "tech-violet",
|
|
284
|
+
"keywords": ["creative", "ui", "ux", "design", "interaction", "threejs", "webgl", "motion", "动效", "视觉", "体验"]
|
|
285
|
+
},
|
|
286
|
+
"--palette-slate-minimal": {
|
|
287
|
+
"name": "极简碳素灰",
|
|
288
|
+
"token": "--palette-slate-minimal",
|
|
289
|
+
"hex": "#334155",
|
|
290
|
+
"tone": "neutral-slate",
|
|
291
|
+
"keywords": ["international", "remote", "global", "overseas", "minimal", "compact", "dense", "外企", "海外", "远程", "极简"]
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
def recommend_palette(role: str, template: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
296
|
+
"""Recommend official palette preset based on role keywords and template style."""
|
|
297
|
+
role_lower = (role or "").lower()
|
|
298
|
+
template_tone = (template.get("visualStyle", {}).get("tone", "") if template else "").lower()
|
|
299
|
+
template_cat = (template.get("roleCategory", "") if template else "").lower()
|
|
300
|
+
|
|
301
|
+
# 1. Match against palette keywords
|
|
302
|
+
for token, p in PALETTE_PRESETS.items():
|
|
303
|
+
if any(kw in role_lower for kw in p["keywords"]):
|
|
304
|
+
return p
|
|
305
|
+
|
|
306
|
+
# 2. Match template visual tone
|
|
307
|
+
for token, p in PALETTE_PRESETS.items():
|
|
308
|
+
if p["tone"] in template_tone or template_tone in p["tone"]:
|
|
309
|
+
return p
|
|
310
|
+
|
|
311
|
+
# 3. Default fallback
|
|
312
|
+
return PALETTE_PRESETS["--palette-tech-blue"]
|
|
313
|
+
|
|
314
|
+
# ==============================================================================
|
|
315
|
+
# 2. PUBLIC FACADE INTERFACE (DEEP MODULE)
|
|
316
|
+
# ==============================================================================
|
|
317
|
+
|
|
318
|
+
def search_templates(
|
|
319
|
+
role: str,
|
|
320
|
+
style: Optional[str] = None,
|
|
321
|
+
target_pages: int = 1,
|
|
322
|
+
density: str = "balanced",
|
|
323
|
+
keywords: Optional[str] = None,
|
|
324
|
+
engine: str = "hybrid"
|
|
325
|
+
) -> List[Dict[str, Any]]:
|
|
326
|
+
"""
|
|
327
|
+
Search and rank HTML resume templates.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
role: Target job title or direction (e.g., 'AI Agent Engineer', 'Tech Lead')
|
|
331
|
+
style: Preferred visual/layout style (e.g., 'single-column-minimal', 'two-column-split')
|
|
332
|
+
target_pages: Expected page count (1 or 2)
|
|
333
|
+
density: Information density preference ('high', 'balanced', 'spacious')
|
|
334
|
+
keywords: Optional JD keywords for text matching
|
|
335
|
+
engine: Scoring engine ('hybrid' | 'weighted' | 'bm25')
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
List of matching templates sorted by matchScore descending.
|
|
339
|
+
"""
|
|
340
|
+
query = SearchQuery(
|
|
341
|
+
role=role,
|
|
342
|
+
style=style,
|
|
343
|
+
target_pages=target_pages,
|
|
344
|
+
density=density,
|
|
345
|
+
keywords=keywords,
|
|
346
|
+
engine=engine
|
|
347
|
+
)
|
|
348
|
+
|
|
108
349
|
templates = load_templates()
|
|
350
|
+
if not templates:
|
|
351
|
+
return []
|
|
352
|
+
|
|
353
|
+
# Select Scorer Strategy
|
|
354
|
+
scorer: BaseTemplateScorer
|
|
355
|
+
if engine == "bm25":
|
|
356
|
+
scorer = BM25TextScorer(templates)
|
|
357
|
+
elif engine == "weighted":
|
|
358
|
+
scorer = WeightedRuleScorer()
|
|
359
|
+
else:
|
|
360
|
+
scorer = HybridTemplateScorer()
|
|
361
|
+
|
|
109
362
|
results = []
|
|
110
|
-
|
|
111
363
|
for t in templates:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
364
|
+
t_copy = dict(t)
|
|
365
|
+
t_copy["recommendedPalette"] = recommend_palette(role, t)
|
|
366
|
+
score_val = scorer.score(t, query)
|
|
367
|
+
t_copy["matchScore"] = score_val
|
|
368
|
+
t_copy["score"] = score_val
|
|
369
|
+
t_copy["template"] = t
|
|
370
|
+
results.append(t_copy)
|
|
117
371
|
|
|
118
|
-
results.sort(key=lambda x: x
|
|
372
|
+
results.sort(key=lambda x: x.get("matchScore", 0.0), reverse=True)
|
|
119
373
|
return results
|
|
120
|
-
|
|
121
374
|
def main():
|
|
122
375
|
parser = argparse.ArgumentParser(description="KnowMe CareerForge — Template Search Engine")
|
|
123
|
-
parser.add_argument("role", nargs="?", default=
|
|
124
|
-
parser.add_argument("--
|
|
125
|
-
parser.add_argument("--
|
|
126
|
-
parser.add_argument("--
|
|
127
|
-
parser.add_argument("--
|
|
128
|
-
|
|
376
|
+
parser.add_argument("role", nargs="?", default=None, help="Target role or job title (e.g. 'AI Agent Engineer')")
|
|
377
|
+
parser.add_argument("--role", "-r", dest="named_role", default=None, help="Target role (named option)")
|
|
378
|
+
parser.add_argument("--style", "-s", default=None, help="Style preference (minimal, modern, executive, classic)")
|
|
379
|
+
parser.add_argument("--target-pages", "-p", type=int, default=1, help="Target pages (1 or 2)")
|
|
380
|
+
parser.add_argument("--density", "-d", default="balanced", choices=["high", "balanced", "spacious"], help="Density")
|
|
381
|
+
parser.add_argument("--keywords", "-k", default=None, help="Additional JD keywords")
|
|
382
|
+
parser.add_argument("--engine", "-e", default="hybrid", choices=["hybrid", "weighted", "bm25"], help="Scoring engine")
|
|
383
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON array")
|
|
384
|
+
|
|
129
385
|
args = parser.parse_args()
|
|
130
|
-
|
|
386
|
+
target_role = args.named_role or args.role
|
|
387
|
+
if not target_role:
|
|
388
|
+
parser.error("the following arguments are required: role (or --role / -r)")
|
|
131
389
|
|
|
390
|
+
results = search_templates(
|
|
391
|
+
role=target_role,
|
|
392
|
+
style=args.style,
|
|
393
|
+
target_pages=args.target_pages,
|
|
394
|
+
density=args.density,
|
|
395
|
+
keywords=args.keywords,
|
|
396
|
+
engine=args.engine
|
|
397
|
+
)
|
|
398
|
+
|
|
132
399
|
if args.json:
|
|
133
400
|
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
134
401
|
return
|
|
135
|
-
|
|
402
|
+
|
|
136
403
|
print("=" * 80)
|
|
137
|
-
print(f" KnowMe CareerForge — Template
|
|
404
|
+
print(f" KnowMe CareerForge — Template Search Engine Results")
|
|
405
|
+
print(f" Query Role : {target_role}")
|
|
406
|
+
print(f" Style Filter : {args.style or 'Any'}")
|
|
407
|
+
print(f" Engine : {args.engine.upper()}")
|
|
408
|
+
print(f" Matches : {len(results)} template(s) found")
|
|
138
409
|
print("=" * 80)
|
|
410
|
+
|
|
139
411
|
for idx, r in enumerate(results, 1):
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
print(f"
|
|
143
|
-
print(f"
|
|
144
|
-
print(f"
|
|
145
|
-
print(f" Directory: {
|
|
412
|
+
custom_tokens = r.get("customizableTokens", [])
|
|
413
|
+
tokens_preview = ", ".join(custom_tokens[:4]) + ("..." if len(custom_tokens) > 4 else "")
|
|
414
|
+
print(f"\n{idx}. [{r.get('matchScore')} pts] {r.get('name')} (ID: {r.get('id')})")
|
|
415
|
+
print(f" Category: {r.get('roleCategory')} | Style: {r.get('style')} | ATS Tier: {r.get('atsScoreTier')}")
|
|
416
|
+
print(f" Target Pages: {r.get('layout', {}).get('targetPages', 1)} | Density: {r.get('layout', {}).get('density')}")
|
|
417
|
+
print(f" Directory: {r.get('_directory')}")
|
|
418
|
+
pal = r.get("recommendedPalette")
|
|
419
|
+
if pal:
|
|
420
|
+
print(f" Palette : {pal.get('name')} ({pal.get('hex')}) [Token: {pal.get('token')}]")
|
|
421
|
+
if tokens_preview:
|
|
422
|
+
print(f" Tokens : {tokens_preview}")
|
|
146
423
|
print("-" * 80)
|
|
147
424
|
|
|
425
|
+
|
|
148
426
|
if __name__ == "__main__":
|
|
149
427
|
main()
|