project-interview-skill 1.1.0
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/LICENSE +35 -0
- package/README.md +188 -0
- package/SKILL.md +208 -0
- package/bin/project-interview-skill.js +470 -0
- package/package.json +33 -0
- package/references/interview-rubric.md +41 -0
- package/references/oral-and-resume-patterns.md +26 -0
- package/references/oral-style-samples.md +59 -0
- package/references/output-templates.md +77 -0
- package/references/star-framework.md +29 -0
- package/scripts/build_prompt.py +202 -0
- package/scripts/check_inputs.py +179 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# 输出骨架(仓库根目录两个文件)
|
|
2
|
+
|
|
3
|
+
约定:**工作区根目录**(用户当前打开的项目根)生成:
|
|
4
|
+
|
|
5
|
+
- `导学-{简称}.md`
|
|
6
|
+
- `面经-{简称}.md`
|
|
7
|
+
|
|
8
|
+
`{简称}` 由用户在输入中给出,或由你从项目描述中提炼(2~8 个字符为宜),并在文首「已确认输入」中写明。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## `导学-{简称}.md` 骨架
|
|
13
|
+
|
|
14
|
+
```markdown
|
|
15
|
+
# 导学:{简称}
|
|
16
|
+
|
|
17
|
+
## 1. 前置知识(面试高频标注)
|
|
18
|
+
| 知识点 | 为何需要 | 在本项目中的位置 | 高频度 |
|
|
19
|
+
|
|
20
|
+
## 2. 重点亮点与学习顺序(先看这个)
|
|
21
|
+
- (3~6 条:亮点标题 / 为什么重要 / 先看哪些文件(相对路径)/ 建议学习顺序)
|
|
22
|
+
|
|
23
|
+
## 3. 必备知识点
|
|
24
|
+
- ( checklist:必须搞懂的点,可与上表呼应)
|
|
25
|
+
|
|
26
|
+
## 4. 推荐阅读(结合仓库)
|
|
27
|
+
> 每条必须包含 **项目相对路径**(如 `src/views/xxx.vue`),无代码则写 `(仓库未提供路径,待用户补充)`。
|
|
28
|
+
|
|
29
|
+
| 主题 | 建议阅读的代码/文档位置 | 预计时间 | 读完能回答什么 |
|
|
30
|
+
|
|
31
|
+
## 5. 自学提醒
|
|
32
|
+
- 若某文件或原理看不懂,请继续追问 AI;本 skill 负责给学习路径与题目,不提供逐行讲解。
|
|
33
|
+
|
|
34
|
+
## 6. 项目技术定位
|
|
35
|
+
- 倾向:前端 / 后端 / AI / 交叉;依据一句话
|
|
36
|
+
|
|
37
|
+
## 7. 核心原理解析
|
|
38
|
+
### 7.x 主题
|
|
39
|
+
- 问题 → 机制 → 在本项目中的落点
|
|
40
|
+
|
|
41
|
+
## 8. 关键设计决策
|
|
42
|
+
| 决策点 | 备选 | 取舍 | 风险 | 验证 |
|
|
43
|
+
|
|
44
|
+
## 9. 量化与验证(含待测,建议)
|
|
45
|
+
- 文中涉及优化/性能/错误率等:无实测数据处标注 **(待测)**
|
|
46
|
+
- **可选放在本文件末尾**:列出与本项目相关的 **如何测**(性能优化场景建议写:环境、指标、工具、基线/对比注意点);无则写「本项目量化点较少,面试诚实说明」。
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## `面经-{简称}.md` 骨架
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
# 面经:{简称}
|
|
55
|
+
|
|
56
|
+
## 项目简介(简历可用,1~2 句)
|
|
57
|
+
> 基于……的……;用户能……;支持……(参见同目录 `oral-and-resume-patterns.md`)
|
|
58
|
+
|
|
59
|
+
## 简历 bullet(4~6 条)
|
|
60
|
+
- 动作 + 技术/约束 + 结果或(待测)
|
|
61
|
+
|
|
62
|
+
## 面试问题(按频率排序)
|
|
63
|
+
> 正文重心放在面试题口播,不单独输出「亮点拆解」章节;建议 3~6 个主题,每主题至少 1 主问 + 2 追问。
|
|
64
|
+
|
|
65
|
+
### Q1(频率:高)[标签/主题]
|
|
66
|
+
**面试官问题**:……
|
|
67
|
+
|
|
68
|
+
**口播版(第一人称,≥150 字,完整 STAR)**
|
|
69
|
+
(场景→归因→动作→结果/兜底,可含英文术语括号)
|
|
70
|
+
|
|
71
|
+
**追问 1**:……
|
|
72
|
+
**口播版(≥150 字)**:……
|
|
73
|
+
|
|
74
|
+
**追问 2**:……
|
|
75
|
+
**口播版(≥150 字)**:……
|
|
76
|
+
|
|
77
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# STAR、追问与口播要求
|
|
2
|
+
|
|
3
|
+
## 口播版(面经中的答题正文)
|
|
4
|
+
|
|
5
|
+
- **第一人称**:面经文档里「主问题 / 追问」的**口播正文**一律用「我」作主语(协作题可一句内用「我们」说明边界)。
|
|
6
|
+
- **字数**:每一道**主问题**、每一道**追问**的口播版均 **≥150 个汉字**(或等价符号不计入时以汉字为主),且须能单独讲清**完整 STAR**(情境—任务—行动—结果),禁止只列关键词。
|
|
7
|
+
- **结构**:推荐「场景(现象)→ 归因(根因)→ 动作(分层方案)→ 结果/兜底/待测」;详见 [口播与简历抽象](oral-and-resume-patterns.md)。
|
|
8
|
+
- **术语**:关键 API、库名、协议可加英文括号,如:`JSON.parse`、`safeParse`。
|
|
9
|
+
- **待测数据**:口播中涉及性能、错误率等若暂无实测,用 **(待测)** 标出;在该文件末尾「量化与验证」中写清如何测(见 SKILL 输出契约)。
|
|
10
|
+
|
|
11
|
+
## STAR 四要素(口播内需隐含或显式点到)
|
|
12
|
+
|
|
13
|
+
- **S**:业务与现场约束(上线初期、某环境、流量特征)。
|
|
14
|
+
- **T**:你的目标与可验证标准(哪怕定性)。
|
|
15
|
+
- **A**:你具体落地的步骤与权衡(为何不用更简单的做法)。
|
|
16
|
+
- **R**:结果、指标或(待测)+ 后续观测/降级。
|
|
17
|
+
|
|
18
|
+
## 主问题设计
|
|
19
|
+
|
|
20
|
+
- 从**最大风险或最难权衡**切入,能自然引出架构与细节。
|
|
21
|
+
|
|
22
|
+
## 追问递进(每个亮点固定 2 个)
|
|
23
|
+
|
|
24
|
+
1. **细节**:数据结构、API、边界、错误码、一致性。
|
|
25
|
+
2. **失败与演化**:线上问题、回滚、债务、重做会改什么。
|
|
26
|
+
|
|
27
|
+
## 频率标注
|
|
28
|
+
|
|
29
|
+
- 题目排序:**岗位核心栈 > 跨栈基础 > 拓展**;标签 `通用` / `前端` / `后端` / `AI`。
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Build a single consolidated prompt block for generating 导学 + 面经 Markdown files.
|
|
4
|
+
|
|
5
|
+
Reads fields from CLI flags or a JSON file and prints UTF-8 Markdown for agent consumption.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
ILLEGAL_SHORT_NAME_CHARS = {"/", "\\", ":", "*", "?", '"', "<", ">", "|"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _escape_md(text: str) -> str:
|
|
20
|
+
"""Avoid accidental markdown break when embedding user text."""
|
|
21
|
+
return text.replace("```", "``\u200b`")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _validate_short_name(short_name: str) -> str:
|
|
25
|
+
"""
|
|
26
|
+
Validate filename-safe short name used in 导学-{简称}.md / 面经-{简称}.md.
|
|
27
|
+
"""
|
|
28
|
+
cleaned = short_name.strip()
|
|
29
|
+
if not cleaned:
|
|
30
|
+
return ""
|
|
31
|
+
if cleaned in {".", ".."}:
|
|
32
|
+
raise ValueError("short name cannot be '.' or '..'.")
|
|
33
|
+
if any(ch in cleaned for ch in ILLEGAL_SHORT_NAME_CHARS):
|
|
34
|
+
chars = "".join(sorted(ILLEGAL_SHORT_NAME_CHARS))
|
|
35
|
+
raise ValueError(f"short name contains illegal path chars. disallowed: {chars}")
|
|
36
|
+
if len(cleaned) > 24:
|
|
37
|
+
raise ValueError("short name is too long (max 24 characters).")
|
|
38
|
+
return cleaned
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_prompt(
|
|
42
|
+
description: str,
|
|
43
|
+
tech_stack: str | None,
|
|
44
|
+
role_focus: str | None,
|
|
45
|
+
short_name: str | None,
|
|
46
|
+
extra: str | None,
|
|
47
|
+
) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Compose the master prompt for the analyzing agent.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
description: Required project description.
|
|
53
|
+
tech_stack: Optional stack string.
|
|
54
|
+
role_focus: Optional frontend/backend/AI.
|
|
55
|
+
short_name: Optional short name for `导学-{简称}.md` / `面经-{简称}.md`.
|
|
56
|
+
extra: Optional free-form notes.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Markdown string to paste into a chat or pipe to an LLM.
|
|
60
|
+
"""
|
|
61
|
+
lines: list[str] = [
|
|
62
|
+
"请严格按 `project-interview-skill` 执行:在**工作区根目录**写入两个文件:",
|
|
63
|
+
"`导学-{简称}.md` 与 `面经-{简称}.md`;量化与验证仅作为导学中的可选建议项。",
|
|
64
|
+
"",
|
|
65
|
+
"## 已确认输入",
|
|
66
|
+
"",
|
|
67
|
+
"### 项目简称(用于文件名)",
|
|
68
|
+
"",
|
|
69
|
+
]
|
|
70
|
+
if short_name and short_name.strip():
|
|
71
|
+
lines.append(short_name.strip())
|
|
72
|
+
else:
|
|
73
|
+
lines.append("_(未提供,请从描述提炼并在写入前写明)_")
|
|
74
|
+
lines.extend(["", "### 项目描述(必须)", "", "```text", _escape_md(description.strip()), "```", ""])
|
|
75
|
+
|
|
76
|
+
lines.append("### 技术栈(可选)")
|
|
77
|
+
lines.append("")
|
|
78
|
+
if tech_stack and tech_stack.strip():
|
|
79
|
+
lines.append("```text")
|
|
80
|
+
lines.append(_escape_md(tech_stack.strip()))
|
|
81
|
+
lines.append("```")
|
|
82
|
+
else:
|
|
83
|
+
lines.append("_(未提供,请合理推断并列出假设)_")
|
|
84
|
+
lines.append("")
|
|
85
|
+
|
|
86
|
+
lines.append("### 求职方向(可选)")
|
|
87
|
+
lines.append("")
|
|
88
|
+
if role_focus and role_focus.strip():
|
|
89
|
+
lines.append(role_focus.strip())
|
|
90
|
+
else:
|
|
91
|
+
lines.append("_(未提供,请从描述推断或给出交叉标签)_")
|
|
92
|
+
lines.append("")
|
|
93
|
+
|
|
94
|
+
if extra and extra.strip():
|
|
95
|
+
lines.append("### 补充说明")
|
|
96
|
+
lines.append("")
|
|
97
|
+
lines.append(extra.strip())
|
|
98
|
+
lines.append("")
|
|
99
|
+
|
|
100
|
+
lines.extend(
|
|
101
|
+
[
|
|
102
|
+
"## 输出要求(摘要)",
|
|
103
|
+
"",
|
|
104
|
+
"- 面经:项目简介 1~2 句(简历向);主问题 15~25;追问不计入总数;建议按 3~6 个主题组织,每主题至少 1 主问 + 2 追问。",
|
|
105
|
+
"- 面经:正文重心放在面试题口播,不单独输出「亮点拆解」章节。",
|
|
106
|
+
"- 口播:第一人称;主问与每个追问口播均 **≥150 字**,完整 STAR(场景→归因→动作→结果/兜底)。",
|
|
107
|
+
"- 导学:必须包含「重点亮点与学习顺序」和「推荐阅读」表,含 **仓库相对路径**。",
|
|
108
|
+
"- 导学:加一条固定自学提醒——看不懂继续问 AI,本 skill 不做逐行讲解。",
|
|
109
|
+
"- 量化与验证:仅导学可选添加(建议语气);面经不强制该章节。",
|
|
110
|
+
"",
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
return "\n".join(lines)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _load_json(path: Path) -> dict[str, Any]:
|
|
117
|
+
raw = path.read_text(encoding="utf-8")
|
|
118
|
+
data = json.loads(raw)
|
|
119
|
+
if not isinstance(data, dict):
|
|
120
|
+
raise ValueError("JSON root must be an object")
|
|
121
|
+
return data
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main(argv: list[str] | None = None) -> int:
|
|
125
|
+
"""CLI entry point."""
|
|
126
|
+
parser = argparse.ArgumentParser(
|
|
127
|
+
description="Build consolidated prompt for project-interview-skill."
|
|
128
|
+
)
|
|
129
|
+
parser.add_argument(
|
|
130
|
+
"--description",
|
|
131
|
+
"-d",
|
|
132
|
+
help="Project description text.",
|
|
133
|
+
)
|
|
134
|
+
parser.add_argument(
|
|
135
|
+
"--short-name",
|
|
136
|
+
"-s",
|
|
137
|
+
default="",
|
|
138
|
+
help="Short name for filenames: 导学-{简称}.md / 面经-{简称}.md",
|
|
139
|
+
)
|
|
140
|
+
parser.add_argument("--tech", default="", help="Technology stack string.")
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"--role",
|
|
143
|
+
default="",
|
|
144
|
+
help="Job direction, e.g. 前端 / 后端 / AI.",
|
|
145
|
+
)
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"--extra",
|
|
148
|
+
default="",
|
|
149
|
+
help="Additional notes appended under 补充说明.",
|
|
150
|
+
)
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"--json-file",
|
|
153
|
+
help="Path to JSON with keys: description, short_name?, tech_stack?, role_focus?, extra?.",
|
|
154
|
+
)
|
|
155
|
+
args = parser.parse_args(argv)
|
|
156
|
+
|
|
157
|
+
description = args.description or ""
|
|
158
|
+
tech = args.tech or ""
|
|
159
|
+
role = args.role or ""
|
|
160
|
+
short_name = args.short_name or ""
|
|
161
|
+
extra = args.extra or ""
|
|
162
|
+
|
|
163
|
+
if args.json_file:
|
|
164
|
+
try:
|
|
165
|
+
payload = _load_json(Path(args.json_file))
|
|
166
|
+
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
|
167
|
+
print(f"Error loading JSON: {exc}", file=sys.stderr)
|
|
168
|
+
return 1
|
|
169
|
+
description = str(payload.get("description", "")).strip()
|
|
170
|
+
tech = str(payload.get("tech_stack", payload.get("tech", ""))).strip()
|
|
171
|
+
role = str(payload.get("role_focus", payload.get("role", ""))).strip()
|
|
172
|
+
short_name = str(payload.get("short_name", payload.get("简称", ""))).strip()
|
|
173
|
+
extra = str(payload.get("extra", "")).strip()
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
short_name = _validate_short_name(short_name)
|
|
177
|
+
except ValueError as exc:
|
|
178
|
+
print(f"Error: invalid --short-name: {exc}", file=sys.stderr)
|
|
179
|
+
return 1
|
|
180
|
+
|
|
181
|
+
if not description:
|
|
182
|
+
print(
|
|
183
|
+
"Error: missing description. Use -d, or --json-file with description field.",
|
|
184
|
+
file=sys.stderr,
|
|
185
|
+
)
|
|
186
|
+
return 1
|
|
187
|
+
|
|
188
|
+
prompt = build_prompt(
|
|
189
|
+
description,
|
|
190
|
+
tech or None,
|
|
191
|
+
role or None,
|
|
192
|
+
short_name or None,
|
|
193
|
+
extra or None,
|
|
194
|
+
)
|
|
195
|
+
sys.stdout.write(prompt)
|
|
196
|
+
if not prompt.endswith("\n"):
|
|
197
|
+
sys.stdout.write("\n")
|
|
198
|
+
return 0
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Validate project narrative inputs for project-interview-skill.
|
|
4
|
+
|
|
5
|
+
Reads UTF-8 text from stdin or a file, checks for minimum signal,
|
|
6
|
+
and prints suggested follow-up questions in Chinese.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from dataclasses import asdict, dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Iterable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
MIN_DESCRIPTION_CHARS = 40
|
|
20
|
+
KEYWORD_HINTS = (
|
|
21
|
+
"指标",
|
|
22
|
+
"qps",
|
|
23
|
+
"延迟",
|
|
24
|
+
"用户",
|
|
25
|
+
"线上",
|
|
26
|
+
"数据",
|
|
27
|
+
"规模",
|
|
28
|
+
"难点",
|
|
29
|
+
"结果",
|
|
30
|
+
"职责",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class CheckResult:
|
|
36
|
+
"""Outcome of input validation."""
|
|
37
|
+
|
|
38
|
+
ok: bool
|
|
39
|
+
missing: list[str] = field(default_factory=list)
|
|
40
|
+
suggestions: list[str] = field(default_factory=list)
|
|
41
|
+
notes: list[str] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _text_length(text: str) -> int:
|
|
45
|
+
"""Character count after strip."""
|
|
46
|
+
return len(text.strip())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _has_any_keyword(text: str, keywords: Iterable[str]) -> bool:
|
|
50
|
+
lower = text.lower()
|
|
51
|
+
for kw in keywords:
|
|
52
|
+
if kw.lower() in lower:
|
|
53
|
+
return True
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def analyze(description: str, tech_stack: str | None, role_focus: str | None) -> CheckResult:
|
|
58
|
+
"""
|
|
59
|
+
Analyze user-provided fields and return gaps plus follow-up suggestions.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
description: Required project description.
|
|
63
|
+
tech_stack: Optional technology list or sentence.
|
|
64
|
+
role_focus: Optional job direction (frontend/backend/AI).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
CheckResult with ok flag, missing topics, and concrete questions.
|
|
68
|
+
"""
|
|
69
|
+
missing: list[str] = []
|
|
70
|
+
suggestions: list[str] = []
|
|
71
|
+
notes: list[str] = []
|
|
72
|
+
|
|
73
|
+
desc_len = _text_length(description)
|
|
74
|
+
if desc_len < MIN_DESCRIPTION_CHARS:
|
|
75
|
+
missing.append(
|
|
76
|
+
f"项目描述过短(当前约 {desc_len} 字符,建议至少 {MIN_DESCRIPTION_CHARS}+ "
|
|
77
|
+
"并包含职责、难点或结果线索)"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if not _has_any_keyword(description, KEYWORD_HINTS) and desc_len < MIN_DESCRIPTION_CHARS * 2:
|
|
81
|
+
suggestions.append(
|
|
82
|
+
"请补充:你在项目中的**具体职责**(独立负责模块 / 协作边界)?"
|
|
83
|
+
)
|
|
84
|
+
suggestions.append(
|
|
85
|
+
"请补充:一个**技术或业务难点**,以及你如何验证「已解决」?"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if not tech_stack or not tech_stack.strip():
|
|
89
|
+
suggestions.append(
|
|
90
|
+
"技术栈未提供:若补充语言/框架/存储/消息/观测栈,面试题会更准。"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if not role_focus or not role_focus.strip():
|
|
94
|
+
suggestions.append(
|
|
95
|
+
"求职方向未指定:目标是 **前端 / 后端 / AI** 中的哪些?会影响题目权重与标签。"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
suggestions.extend(
|
|
99
|
+
[
|
|
100
|
+
"请描述一次**线上问题或惊险排障**:现象、定位手段、修复、复盘项?",
|
|
101
|
+
"若重新做一遍,你会在哪个**设计决策**上改变?为什么?",
|
|
102
|
+
"项目中**如何发布与回滚**(灰度、特性开关、数据迁移)?",
|
|
103
|
+
]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if desc_len >= MIN_DESCRIPTION_CHARS:
|
|
107
|
+
notes.append("项目描述长度达标;可按追问补齐量化与线上证据。")
|
|
108
|
+
|
|
109
|
+
ok = len(missing) == 0
|
|
110
|
+
return CheckResult(ok=ok, missing=missing, suggestions=suggestions, notes=notes)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(argv: list[str] | None = None) -> int:
|
|
114
|
+
"""CLI entry point."""
|
|
115
|
+
parser = argparse.ArgumentParser(
|
|
116
|
+
description="Check project description completeness for project-interview-skill."
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"-f",
|
|
120
|
+
"--file",
|
|
121
|
+
help="Read project description from file (UTF-8).",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--tech",
|
|
125
|
+
default="",
|
|
126
|
+
help="Optional tech stack string.",
|
|
127
|
+
)
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--role",
|
|
130
|
+
default="",
|
|
131
|
+
help="Optional job focus: frontend, backend, AI, etc.",
|
|
132
|
+
)
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--json",
|
|
135
|
+
action="store_true",
|
|
136
|
+
help="Print machine-readable JSON.",
|
|
137
|
+
)
|
|
138
|
+
args = parser.parse_args(argv)
|
|
139
|
+
|
|
140
|
+
if args.file:
|
|
141
|
+
try:
|
|
142
|
+
description = Path(args.file).read_text(encoding="utf-8").strip()
|
|
143
|
+
except OSError as exc:
|
|
144
|
+
print(f"Error reading file: {exc}", file=sys.stderr)
|
|
145
|
+
return 1
|
|
146
|
+
else:
|
|
147
|
+
description = sys.stdin.read().strip()
|
|
148
|
+
|
|
149
|
+
if not description:
|
|
150
|
+
print("Error: empty description. Pipe text or use --file.", file=sys.stderr)
|
|
151
|
+
return 1
|
|
152
|
+
|
|
153
|
+
tech = args.tech.strip() or None
|
|
154
|
+
role = args.role.strip() or None
|
|
155
|
+
result = analyze(description, tech, role)
|
|
156
|
+
|
|
157
|
+
if args.json:
|
|
158
|
+
print(json.dumps(asdict(result), ensure_ascii=False, indent=2))
|
|
159
|
+
return 0
|
|
160
|
+
|
|
161
|
+
status = "PASS" if result.ok else "NEEDS_INPUT"
|
|
162
|
+
print(f"[{status}] project-interview-skill input check\n")
|
|
163
|
+
if result.missing:
|
|
164
|
+
print("必须补齐:")
|
|
165
|
+
for item in result.missing:
|
|
166
|
+
print(f" - {item}")
|
|
167
|
+
print()
|
|
168
|
+
print("建议追问:")
|
|
169
|
+
for q in result.suggestions:
|
|
170
|
+
print(f" - {q}")
|
|
171
|
+
if result.notes:
|
|
172
|
+
print("\n备注:")
|
|
173
|
+
for n in result.notes:
|
|
174
|
+
print(f" - {n}")
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
raise SystemExit(main())
|