workplace-pua-cli 0.4.1 → 0.8.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 +21 -0
- package/README.md +512 -243
- package/dist/commands/chat-new-imports.js +2 -0
- package/dist/commands/config.js +14 -6
- package/dist/commands/email.js +301 -0
- package/dist/commands/interview.js +660 -0
- package/dist/commands/jargon.js +153 -0
- package/dist/commands/meeting-room.js +384 -0
- package/dist/commands/meeting.js +323 -0
- package/dist/commands/weekly.js +302 -0
- package/dist/index.js +29 -7
- package/dist/prompts/hr.js +126 -0
- package/dist/prompts/index.js +82 -1
- package/dist/prompts/intern.js +126 -0
- package/dist/prompts/interview-prompts.js +286 -0
- package/dist/prompts/meeting-prompts.js +229 -0
- package/dist/prompts/pm.js +123 -0
- package/dist/prompts/techlead.js +126 -0
- package/dist/utils/box.js +141 -0
- package/dist/utils/meeting-utils.js +194 -0
- package/dist/utils/resume-parser.js +122 -0
- package/dist/utils/stream.js +97 -13
- package/dist/utils/theme.js +177 -0
- package/package.json +73 -52
- package/.env.example +0 -4
- package/.eslintrc.json +0 -21
- package/.prettierrc.json +0 -9
- package/CHANGELOG.md +0 -113
- package/docs/OPTIMIZATION.md +0 -772
- package/docs/TECHNICAL_PRINCIPLES.md +0 -663
- package/sample/1.png +0 -0
- package/sample/2.png +0 -0
- package/screenshots/chat-dialogue.png +0 -0
- package/screenshots/chat-mode.png +0 -0
- package/src/__tests__/config/settings.test.ts +0 -48
- package/src/__tests__/prompts/boss.test.ts +0 -35
- package/src/commands/chat.ts +0 -328
- package/src/commands/config.ts +0 -283
- package/src/commands/prompt.ts +0 -154
- package/src/config/providers.ts +0 -109
- package/src/config/session-storage.ts +0 -94
- package/src/config/settings.ts +0 -194
- package/src/config/storage.ts +0 -150
- package/src/history/session.ts +0 -141
- package/src/index.ts +0 -164
- package/src/llm/base.ts +0 -55
- package/src/llm/factory.ts +0 -24
- package/src/llm/openai.ts +0 -113
- package/src/llm/zhipu.ts +0 -101
- package/src/prompts/boss.ts +0 -43
- package/src/prompts/employee.ts +0 -43
- package/src/prompts/index.ts +0 -3
- package/src/utils/formatter.ts +0 -104
- package/src/utils/logger.ts +0 -31
- package/src/utils/stream.ts +0 -76
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -18
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 压力面试命令 - 10 轮问答制
|
|
4
|
+
* 用户扮演候选人,面对 2-4 个刁钻面试官
|
|
5
|
+
* 压力值到 100% 游戏结束
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
41
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
42
|
+
};
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.createInterviewCommand = createInterviewCommand;
|
|
45
|
+
const readline_1 = __importDefault(require("readline"));
|
|
46
|
+
const commander_1 = require("commander");
|
|
47
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
48
|
+
const ora_1 = __importDefault(require("ora"));
|
|
49
|
+
const factory_1 = require("../llm/factory");
|
|
50
|
+
const settings_1 = require("../config/settings");
|
|
51
|
+
const interview_prompts_1 = require("../prompts/interview-prompts");
|
|
52
|
+
const logger_1 = require("../utils/logger");
|
|
53
|
+
const resume_parser_1 = require("../utils/resume-parser");
|
|
54
|
+
const INTERVIEWER_COLORS = {
|
|
55
|
+
techlead: chalk_1.default.blue,
|
|
56
|
+
boss: chalk_1.default.red,
|
|
57
|
+
hr: chalk_1.default.magenta,
|
|
58
|
+
pm: chalk_1.default.cyan,
|
|
59
|
+
};
|
|
60
|
+
const INTERVIEWER_EMOJIS = {
|
|
61
|
+
techlead: '💻',
|
|
62
|
+
boss: '👔',
|
|
63
|
+
hr: '💼',
|
|
64
|
+
pm: '📊',
|
|
65
|
+
};
|
|
66
|
+
const MOOD_EMOJIS = {
|
|
67
|
+
sarcastic: '😏',
|
|
68
|
+
pressing: '🤨',
|
|
69
|
+
neutral: '😐',
|
|
70
|
+
cold: '🥶',
|
|
71
|
+
};
|
|
72
|
+
const QUALITY_LABELS = {
|
|
73
|
+
weak: chalk_1.default.red('弱'),
|
|
74
|
+
normal: chalk_1.default.yellow('一般'),
|
|
75
|
+
strong: chalk_1.default.green('强'),
|
|
76
|
+
};
|
|
77
|
+
const MAX_ANSWER_LENGTH = 500;
|
|
78
|
+
const FORBIDDEN_WORDS = ['<script>', 'javascript:', 'onerror=', 'onload=', 'eval(', 'document.cookie'];
|
|
79
|
+
/**
|
|
80
|
+
* 分析面试官情绪标签
|
|
81
|
+
*/
|
|
82
|
+
function getInterviewerMood(content) {
|
|
83
|
+
const sarcasm = ['呵呵', '有意思', '真的吗', '你确定', '就这?', '算了'];
|
|
84
|
+
const pressure = ['追问', '详细说说', '展开讲讲', '底层', '原理', '为什么'];
|
|
85
|
+
const positive = ['不错', '可以', '嗯', '好的', '理解了'];
|
|
86
|
+
let sarcasmScore = 0;
|
|
87
|
+
let pressureScore = 0;
|
|
88
|
+
let positiveScore = 0;
|
|
89
|
+
for (const kw of sarcasm) {
|
|
90
|
+
if (content.includes(kw))
|
|
91
|
+
sarcasmScore++;
|
|
92
|
+
}
|
|
93
|
+
for (const kw of pressure) {
|
|
94
|
+
if (content.includes(kw))
|
|
95
|
+
pressureScore++;
|
|
96
|
+
}
|
|
97
|
+
for (const kw of positive) {
|
|
98
|
+
if (content.includes(kw))
|
|
99
|
+
positiveScore++;
|
|
100
|
+
}
|
|
101
|
+
if (sarcasmScore > positiveScore)
|
|
102
|
+
return 'sarcastic';
|
|
103
|
+
if (pressureScore > positiveScore)
|
|
104
|
+
return 'pressing';
|
|
105
|
+
if (positiveScore > 0)
|
|
106
|
+
return 'neutral';
|
|
107
|
+
return 'cold';
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 验证用户输入
|
|
111
|
+
*/
|
|
112
|
+
function validateAnswer(input) {
|
|
113
|
+
if (input.length > MAX_ANSWER_LENGTH) {
|
|
114
|
+
return { valid: false, error: `回答太长了(最多 ${MAX_ANSWER_LENGTH} 字)` };
|
|
115
|
+
}
|
|
116
|
+
const lower = input.toLowerCase();
|
|
117
|
+
for (const word of FORBIDDEN_WORDS) {
|
|
118
|
+
if (lower.includes(word)) {
|
|
119
|
+
return { valid: false, error: '输入包含不安全的内容' };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { valid: true };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 渲染压力条和自信条
|
|
126
|
+
*/
|
|
127
|
+
function renderStatusBar(state) {
|
|
128
|
+
const stressBar = renderBar(state.stress, 100, 16, 'red');
|
|
129
|
+
const confBar = renderBar(state.confidence, 100, 16, 'green');
|
|
130
|
+
console.log();
|
|
131
|
+
console.log(chalk_1.default.cyan.bold('╔══════════════════════════════════════╗'));
|
|
132
|
+
console.log(chalk_1.default.cyan.bold('║') + chalk_1.default.white.bold(` 🎯 压力面试 - 第 ${state.round}/${state.totalRounds} 轮`) + ' '.repeat(Math.max(0, 18 - String(state.round).length - String(state.totalRounds).length)) + chalk_1.default.cyan.bold('║'));
|
|
133
|
+
console.log(chalk_1.default.cyan.bold('╠══════════════════════════════════════╣'));
|
|
134
|
+
console.log(chalk_1.default.cyan.bold('║') + ` 压力: ${stressBar} ${String(state.stress).padStart(3)}%` + ' ' + chalk_1.default.cyan.bold('║'));
|
|
135
|
+
console.log(chalk_1.default.cyan.bold('║') + ` 自信: ${confBar} ${String(state.confidence).padStart(3)}%` + ' ' + chalk_1.default.cyan.bold('║'));
|
|
136
|
+
console.log(chalk_1.default.cyan.bold('╚══════════════════════════════════════╝'));
|
|
137
|
+
console.log();
|
|
138
|
+
}
|
|
139
|
+
function renderBar(value, max, width, color) {
|
|
140
|
+
const filled = Math.round((value / max) * width);
|
|
141
|
+
const empty = width - filled;
|
|
142
|
+
const colorFn = color === 'red' ? chalk_1.default.red : chalk_1.default.green;
|
|
143
|
+
return '[' + colorFn('█'.repeat(filled)) + chalk_1.default.gray('░'.repeat(empty)) + ']';
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 渲染面试官消息
|
|
147
|
+
*/
|
|
148
|
+
function renderInterviewerMessage(role, content, mood, customInterviewers) {
|
|
149
|
+
const builtinEmoji = INTERVIEWER_EMOJIS[role];
|
|
150
|
+
const custom = customInterviewers?.find(c => c.id === role);
|
|
151
|
+
const emoji = builtinEmoji || custom?.emoji || '🎤';
|
|
152
|
+
const name = (0, interview_prompts_1.getInterviewerName)(role, customInterviewers);
|
|
153
|
+
const title = (0, interview_prompts_1.getInterviewerTitle)(role, customInterviewers);
|
|
154
|
+
const colorFn = INTERVIEWER_COLORS[role] || chalk_1.default.white;
|
|
155
|
+
const moodEmoji = mood ? (MOOD_EMOJIS[mood] || '') : '';
|
|
156
|
+
const header = `${emoji} ${name} (${title})${moodEmoji ? ' ' + moodEmoji : ''}`;
|
|
157
|
+
const width = 50;
|
|
158
|
+
const topLine = colorFn(`┌─ ${header} ${'─'.repeat(Math.max(0, width - header.length - 4))}┐`);
|
|
159
|
+
const bottomLine = colorFn(`└${'─'.repeat(width - 1)}┘`);
|
|
160
|
+
const maxContentWidth = width - 4;
|
|
161
|
+
const lines = wrapText(content, maxContentWidth);
|
|
162
|
+
console.log(topLine);
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
console.log(colorFn('│') + ` ${line.padEnd(maxContentWidth + 1)}` + colorFn('│'));
|
|
165
|
+
}
|
|
166
|
+
console.log(bottomLine);
|
|
167
|
+
console.log();
|
|
168
|
+
}
|
|
169
|
+
function wrapText(text, maxWidth) {
|
|
170
|
+
const lines = [];
|
|
171
|
+
let remaining = text;
|
|
172
|
+
while (remaining.length > maxWidth) {
|
|
173
|
+
let breakIdx = remaining.lastIndexOf(' ', maxWidth);
|
|
174
|
+
if (breakIdx <= 0)
|
|
175
|
+
breakIdx = maxWidth;
|
|
176
|
+
lines.push(remaining.slice(0, breakIdx));
|
|
177
|
+
remaining = remaining.slice(breakIdx).trimStart();
|
|
178
|
+
}
|
|
179
|
+
if (remaining.length > 0) {
|
|
180
|
+
lines.push(remaining);
|
|
181
|
+
}
|
|
182
|
+
return lines.length > 0 ? lines : [''];
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* 渲染事件
|
|
186
|
+
*/
|
|
187
|
+
function renderEvent(text) {
|
|
188
|
+
console.log(chalk_1.default.gray(` ── ${text} ──`));
|
|
189
|
+
console.log();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 渲染结局
|
|
193
|
+
*/
|
|
194
|
+
function renderEnding(state) {
|
|
195
|
+
const ending = (0, interview_prompts_1.getInterviewEnding)(state.stress, state.confidence, state.round);
|
|
196
|
+
console.log();
|
|
197
|
+
console.log(chalk_1.default.yellow.bold('╔════════════════════════════════════════╗'));
|
|
198
|
+
console.log(chalk_1.default.yellow.bold('║') + ` ${ending.emoji} ${chalk_1.default.white.bold(ending.title)}` + ' '.repeat(Math.max(0, 28 - ending.title.length)) + chalk_1.default.yellow.bold('║'));
|
|
199
|
+
console.log(chalk_1.default.yellow.bold('╠════════════════════════════════════════╣'));
|
|
200
|
+
console.log(chalk_1.default.yellow.bold('║') + ' ' + chalk_1.default.yellow.bold('║'));
|
|
201
|
+
// Wrap description
|
|
202
|
+
const descLines = wrapText(ending.description, 36);
|
|
203
|
+
for (const line of descLines) {
|
|
204
|
+
console.log(chalk_1.default.yellow.bold('║') + ` ${line.padEnd(38)}` + chalk_1.default.yellow.bold('║'));
|
|
205
|
+
}
|
|
206
|
+
console.log(chalk_1.default.yellow.bold('║') + ' ' + chalk_1.default.yellow.bold('║'));
|
|
207
|
+
console.log(chalk_1.default.yellow.bold('║') + ` 最终压力: ${chalk_1.default.red(String(state.stress) + '%')}`.padEnd(49) + chalk_1.default.yellow.bold('║'));
|
|
208
|
+
console.log(chalk_1.default.yellow.bold('║') + ` 最终自信: ${chalk_1.default.green(String(state.confidence) + '%')}`.padEnd(49) + chalk_1.default.yellow.bold('║'));
|
|
209
|
+
console.log(chalk_1.default.yellow.bold('║') + ` 坚持轮次: ${state.round}/${state.totalRounds}`.padEnd(38) + chalk_1.default.yellow.bold('║'));
|
|
210
|
+
console.log(chalk_1.default.yellow.bold('║') + ' ' + chalk_1.default.yellow.bold('║'));
|
|
211
|
+
console.log(chalk_1.default.yellow.bold('╚════════════════════════════════════════╝'));
|
|
212
|
+
console.log();
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* 清理 AI 回复 - 去除叙述格式、嵌套引用、角色名前缀
|
|
216
|
+
*/
|
|
217
|
+
function cleanInterviewResponse(raw, currentRole, customInterviewers) {
|
|
218
|
+
let cleaned = raw.trim();
|
|
219
|
+
const builtinNames = Object.values(interview_prompts_1.INTERVIEWER_NAMES);
|
|
220
|
+
const customNames = customInterviewers?.map(c => c.name) || [];
|
|
221
|
+
const allNames = [...builtinNames, ...customNames];
|
|
222
|
+
// 去除嵌套的叙述格式 (名字说:"...") - 循环剥离多层
|
|
223
|
+
for (let i = 0; i < 5; i++) {
|
|
224
|
+
let changed = false;
|
|
225
|
+
for (const name of allNames) {
|
|
226
|
+
const narrativePattern = new RegExp(`[((]${name}说[::]\\s*[""\u201C](.+?)[""\u201D][))]`, 'gs');
|
|
227
|
+
const newCleaned = cleaned.replace(narrativePattern, '$1');
|
|
228
|
+
if (newCleaned !== cleaned) {
|
|
229
|
+
cleaned = newCleaned;
|
|
230
|
+
changed = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const genericPattern = /[((](?:面试官|其他面试官的发言)[::]?\s*[""\u201C]?(.+?)[""\u201D]?[))]/gs;
|
|
234
|
+
const newCleaned2 = cleaned.replace(genericPattern, '$1');
|
|
235
|
+
if (newCleaned2 !== cleaned) {
|
|
236
|
+
cleaned = newCleaned2;
|
|
237
|
+
changed = true;
|
|
238
|
+
}
|
|
239
|
+
if (!changed)
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
// 去除 [角色名]: 或 角色名: 格式
|
|
243
|
+
for (const name of allNames) {
|
|
244
|
+
cleaned = cleaned.replace(new RegExp(`^\\[${name}\\][::]\\s*`, 'g'), '');
|
|
245
|
+
cleaned = cleaned.replace(new RegExp(`^${name}[::]\\s*`, 'g'), '');
|
|
246
|
+
}
|
|
247
|
+
// 去除回复中夹带的其他角色发言
|
|
248
|
+
const currentName = (0, interview_prompts_1.getInterviewerName)(currentRole, customInterviewers);
|
|
249
|
+
for (const name of allNames) {
|
|
250
|
+
if (name === currentName)
|
|
251
|
+
continue;
|
|
252
|
+
cleaned = cleaned.replace(new RegExp(`\\s*\\[${name}\\][::][^\\n]*`, 'g'), '');
|
|
253
|
+
}
|
|
254
|
+
cleaned = cleaned.replace(/^["「""\u201C](.+)["」""\u201D]$/, '$1');
|
|
255
|
+
cleaned = cleaned.trim();
|
|
256
|
+
if (cleaned.length < 2 || cleaned === '...' || cleaned === '……') {
|
|
257
|
+
return '这个问题你再想想。';
|
|
258
|
+
}
|
|
259
|
+
return cleaned;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 选择本轮提问的面试官(1-2人)
|
|
263
|
+
*/
|
|
264
|
+
function selectInterviewers(interviewers, round, severity) {
|
|
265
|
+
// 第一轮:所有面试官轮流自我介绍式提问,取第一个
|
|
266
|
+
if (round === 1) {
|
|
267
|
+
return [interviewers[0]];
|
|
268
|
+
}
|
|
269
|
+
// 高混乱度更可能多人追问
|
|
270
|
+
const count = severity >= 3 ? Math.min(2, interviewers.length) : 1;
|
|
271
|
+
// 轮流 + 随机
|
|
272
|
+
const baseIndex = (round - 1) % interviewers.length;
|
|
273
|
+
const result = [interviewers[baseIndex]];
|
|
274
|
+
if (count > 1 && interviewers.length > 1) {
|
|
275
|
+
const others = interviewers.filter((_, i) => i !== baseIndex);
|
|
276
|
+
result.push(others[Math.floor(Math.random() * others.length)]);
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
function createInterviewCommand() {
|
|
281
|
+
const command = new commander_1.Command('interview')
|
|
282
|
+
.description('压力面试 - 10轮问答制,挺住压力拿到Offer')
|
|
283
|
+
.option('-p, --provider <zhipu|openai>', 'AI 服务提供商')
|
|
284
|
+
.option('-m, --model <model>', '模型名称')
|
|
285
|
+
.option('--resume <path>', '简历PDF路径,解析后定制面试内容');
|
|
286
|
+
command.action(async (options) => {
|
|
287
|
+
try {
|
|
288
|
+
const { checkbox, select } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
289
|
+
console.log();
|
|
290
|
+
console.log(chalk_1.default.red.bold('🎯 压力面试'));
|
|
291
|
+
console.log(chalk_1.default.gray('你是候选人,面对刁钻面试官的连环追问'));
|
|
292
|
+
console.log(chalk_1.default.gray('坚持 10 轮,控制压力值,争取拿到 Offer!'));
|
|
293
|
+
console.log();
|
|
294
|
+
// Step 1: Select position
|
|
295
|
+
const position = await select({
|
|
296
|
+
message: '选择面试岗位',
|
|
297
|
+
choices: Object.entries(interview_prompts_1.POSITION_NAMES).map(([value, name]) => ({
|
|
298
|
+
name,
|
|
299
|
+
value,
|
|
300
|
+
})),
|
|
301
|
+
});
|
|
302
|
+
// Step 2: Ask if user wants custom interviewers
|
|
303
|
+
const customInterviewers = [];
|
|
304
|
+
const wantCustom = await select({
|
|
305
|
+
message: '是否添加自定义面试官?',
|
|
306
|
+
choices: [
|
|
307
|
+
{ name: '不需要,使用内置面试官', value: 'no' },
|
|
308
|
+
{ name: '添加自定义面试官', value: 'yes' },
|
|
309
|
+
],
|
|
310
|
+
});
|
|
311
|
+
if (wantCustom === 'yes') {
|
|
312
|
+
const { input: inputPrompt } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
313
|
+
let addMore = true;
|
|
314
|
+
let customCount = 0;
|
|
315
|
+
while (addMore && customCount < 2) {
|
|
316
|
+
const cName = await inputPrompt({ message: '面试官名字(如:王总)' });
|
|
317
|
+
const cTitle = await inputPrompt({ message: '面试官职位(如:投资总监)' });
|
|
318
|
+
const cPersonality = await inputPrompt({ message: '性格描述(如:质疑商业模式、追问数据、不相信PPT)' });
|
|
319
|
+
const cTags = await inputPrompt({ message: '标签(如:追问数据 / 质疑可行性)' });
|
|
320
|
+
customInterviewers.push({
|
|
321
|
+
id: `custom_${customCount + 1}`,
|
|
322
|
+
name: cName.trim(),
|
|
323
|
+
title: cTitle.trim(),
|
|
324
|
+
personality: cPersonality.trim(),
|
|
325
|
+
tags: cTags.trim(),
|
|
326
|
+
emoji: '🎤',
|
|
327
|
+
});
|
|
328
|
+
customCount++;
|
|
329
|
+
if (customCount < 2) {
|
|
330
|
+
const more = await select({
|
|
331
|
+
message: '继续添加自定义面试官?',
|
|
332
|
+
choices: [
|
|
333
|
+
{ name: '不了', value: 'no' },
|
|
334
|
+
{ name: '再加一个', value: 'yes' },
|
|
335
|
+
],
|
|
336
|
+
});
|
|
337
|
+
addMore = more === 'yes';
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Step 3: Select interviewers (built-in + custom)
|
|
342
|
+
const builtinChoices = ['techlead', 'boss', 'hr', 'pm'].map(role => ({
|
|
343
|
+
name: `${INTERVIEWER_EMOJIS[role]} ${interview_prompts_1.INTERVIEWER_NAMES[role]} (${interview_prompts_1.INTERVIEWER_TITLES[role]}) - ${interview_prompts_1.INTERVIEWER_TAGS[role]}`,
|
|
344
|
+
value: role,
|
|
345
|
+
}));
|
|
346
|
+
const customChoices = customInterviewers.map(c => ({
|
|
347
|
+
name: `🎤 ${c.name} (${c.title}) - ${c.tags}`,
|
|
348
|
+
value: c.id,
|
|
349
|
+
}));
|
|
350
|
+
const interviewers = await checkbox({
|
|
351
|
+
message: '选择面试官(空格选择,2-4 人)',
|
|
352
|
+
choices: [...builtinChoices, ...customChoices],
|
|
353
|
+
});
|
|
354
|
+
if (interviewers.length < 2) {
|
|
355
|
+
logger_1.logger.error('至少需要选择 2 个面试官');
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (interviewers.length > 4) {
|
|
359
|
+
logger_1.logger.error('最多选择 4 个面试官');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Step 4: Select severity
|
|
363
|
+
const severity = await select({
|
|
364
|
+
message: '选择 PUA 强度',
|
|
365
|
+
choices: [
|
|
366
|
+
{ name: '🟢 友好 - 偶尔施压,总体友好', value: 1 },
|
|
367
|
+
{ name: '🟡 标准 - 刁钻追问,不给喘息', value: 2 },
|
|
368
|
+
{ name: '🔴 地狱 - 连珠炮追问,冷嘲热讽', value: 3 },
|
|
369
|
+
],
|
|
370
|
+
});
|
|
371
|
+
// Step 5: Optional candidate profile (from resume or manual input)
|
|
372
|
+
let candidateProfile;
|
|
373
|
+
// Check if --resume flag was provided
|
|
374
|
+
if (options.resume) {
|
|
375
|
+
const resumeSpinner = (0, ora_1.default)({ text: '正在解析简历...', spinner: 'dots' }).start();
|
|
376
|
+
try {
|
|
377
|
+
const resumeText = await (0, resume_parser_1.parseResumePDF)(options.resume);
|
|
378
|
+
candidateProfile = (0, resume_parser_1.extractProfileFromResume)(resumeText);
|
|
379
|
+
resumeSpinner.succeed('简历解析成功!');
|
|
380
|
+
// Show extracted info
|
|
381
|
+
console.log(chalk_1.default.gray(' 提取到的信息:'));
|
|
382
|
+
if (candidateProfile.name)
|
|
383
|
+
console.log(chalk_1.default.gray(` 姓名: ${candidateProfile.name}`));
|
|
384
|
+
if (candidateProfile.experience)
|
|
385
|
+
console.log(chalk_1.default.gray(` 工作年限: ${candidateProfile.experience} 年`));
|
|
386
|
+
if (candidateProfile.techStack)
|
|
387
|
+
console.log(chalk_1.default.gray(` 技术栈: ${candidateProfile.techStack}`));
|
|
388
|
+
if (candidateProfile.targetSalary)
|
|
389
|
+
console.log(chalk_1.default.gray(` 期望薪资: ${candidateProfile.targetSalary}`));
|
|
390
|
+
if (candidateProfile.background)
|
|
391
|
+
console.log(chalk_1.default.gray(` 背景: ${candidateProfile.background}`));
|
|
392
|
+
console.log();
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
resumeSpinner.fail(`简历解析失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
396
|
+
console.log(chalk_1.default.gray(' 将使用手动输入模式'));
|
|
397
|
+
console.log();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// If no resume or resume parsing failed, offer manual input
|
|
401
|
+
if (!candidateProfile) {
|
|
402
|
+
const wantProfile = await select({
|
|
403
|
+
message: '是否填写候选人信息?(让面试更有针对性)',
|
|
404
|
+
choices: [
|
|
405
|
+
{ name: '跳过,直接开始', value: 'no' },
|
|
406
|
+
{ name: '填写我的信息', value: 'yes' },
|
|
407
|
+
],
|
|
408
|
+
});
|
|
409
|
+
if (wantProfile === 'yes') {
|
|
410
|
+
const { input: inputPrompt } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
411
|
+
const pName = await inputPrompt({ message: '你的名字(可选,直接回车跳过)', default: '' });
|
|
412
|
+
const pExp = await inputPrompt({ message: '工作年限(如:3)', default: '' });
|
|
413
|
+
const pStack = await inputPrompt({ message: '技术栈(如:React, TypeScript, Node.js)', default: '' });
|
|
414
|
+
const pSalary = await inputPrompt({ message: '期望薪资(如:25k-30k)', default: '' });
|
|
415
|
+
const pBg = await inputPrompt({ message: '简要背景(如:985本科,3年大厂经验)', default: '' });
|
|
416
|
+
candidateProfile = {
|
|
417
|
+
...(pName ? { name: pName } : {}),
|
|
418
|
+
...(pExp ? { experience: parseInt(pExp) || undefined } : {}),
|
|
419
|
+
...(pStack ? { techStack: pStack } : {}),
|
|
420
|
+
...(pSalary ? { targetSalary: pSalary } : {}),
|
|
421
|
+
...(pBg ? { background: pBg } : {}),
|
|
422
|
+
};
|
|
423
|
+
if (Object.keys(candidateProfile).length === 0) {
|
|
424
|
+
candidateProfile = undefined;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Load config
|
|
429
|
+
const config = (0, settings_1.loadConfig)(options);
|
|
430
|
+
const llm = (0, factory_1.createLLM)(config.provider, {
|
|
431
|
+
apiKey: config.apiKey,
|
|
432
|
+
model: config.model,
|
|
433
|
+
baseUrl: (0, settings_1.getProviderBaseUrl)(config.provider),
|
|
434
|
+
});
|
|
435
|
+
// Initialize state
|
|
436
|
+
const state = {
|
|
437
|
+
interviewers,
|
|
438
|
+
position,
|
|
439
|
+
severity,
|
|
440
|
+
stress: 20, // 初始压力
|
|
441
|
+
confidence: 60, // 初始自信
|
|
442
|
+
round: 1,
|
|
443
|
+
totalRounds: 10,
|
|
444
|
+
messages: [],
|
|
445
|
+
finished: false,
|
|
446
|
+
customInterviewers,
|
|
447
|
+
candidateProfile,
|
|
448
|
+
};
|
|
449
|
+
// Print interview header
|
|
450
|
+
const positionName = interview_prompts_1.POSITION_NAMES[position];
|
|
451
|
+
const interviewerNames = interviewers.map(r => {
|
|
452
|
+
const builtinEmoji = INTERVIEWER_EMOJIS[r];
|
|
453
|
+
const custom = customInterviewers.find(c => c.id === r);
|
|
454
|
+
const emoji = builtinEmoji || custom?.emoji || '🎤';
|
|
455
|
+
const name = (0, interview_prompts_1.getInterviewerName)(r, customInterviewers);
|
|
456
|
+
return `${emoji} ${name}`;
|
|
457
|
+
}).join(' ');
|
|
458
|
+
const severityLabels = { 1: '友好', 2: '标准', 3: '地狱' };
|
|
459
|
+
console.log();
|
|
460
|
+
console.log(chalk_1.default.red.bold('╔══════════════════════════════════════════════════╗'));
|
|
461
|
+
console.log(chalk_1.default.red.bold('║') + chalk_1.default.white.bold(` 🎯 ${positionName}岗位 - 压力面试开始!`) + ' '.repeat(Math.max(0, 14 - positionName.length)) + chalk_1.default.red.bold('║'));
|
|
462
|
+
console.log(chalk_1.default.red.bold('╠══════════════════════════════════════════════════╣'));
|
|
463
|
+
console.log(chalk_1.default.red.bold('║') + ` 面试官: ${interviewerNames}`);
|
|
464
|
+
console.log(chalk_1.default.red.bold('║') + ` 强度: ${severityLabels[severity]} | 回合: 10 轮`);
|
|
465
|
+
console.log(chalk_1.default.red.bold('╚══════════════════════════════════════════════════╝'));
|
|
466
|
+
console.log();
|
|
467
|
+
console.log(chalk_1.default.gray('输入回答面试官的问题。支持: /quit 放弃 | /status 查看状态'));
|
|
468
|
+
console.log();
|
|
469
|
+
// Show initial status
|
|
470
|
+
renderStatusBar(state);
|
|
471
|
+
// First round: interviewer asks opening question
|
|
472
|
+
const spinner = (0, ora_1.default)({ text: '面试官正在思考问题...', spinner: 'dots' }).start();
|
|
473
|
+
const openingInterviewer = interviewers[0];
|
|
474
|
+
const openingPrompt = (0, interview_prompts_1.getInterviewPrompt)(openingInterviewer, position, severity, state.round, state.totalRounds, state.stress, interviewers, customInterviewers, candidateProfile);
|
|
475
|
+
try {
|
|
476
|
+
const openingMsg = await llm.chat([
|
|
477
|
+
{ role: 'system', content: openingPrompt },
|
|
478
|
+
{ role: 'user', content: `(这是面试的第一轮。请向候选人提出第一个问题。候选人面试的是${positionName}岗位。)` },
|
|
479
|
+
]);
|
|
480
|
+
spinner.stop();
|
|
481
|
+
const cleaned = cleanInterviewResponse(openingMsg, openingInterviewer, customInterviewers);
|
|
482
|
+
const mood = getInterviewerMood(cleaned);
|
|
483
|
+
renderInterviewerMessage(openingInterviewer, cleaned, mood, customInterviewers);
|
|
484
|
+
state.messages.push({ role: openingInterviewer, name: (0, interview_prompts_1.getInterviewerName)(openingInterviewer, customInterviewers), content: cleaned });
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
spinner.stop();
|
|
488
|
+
const fallback = '请先做个自我介绍吧。';
|
|
489
|
+
renderInterviewerMessage(openingInterviewer, fallback, 'cold', customInterviewers);
|
|
490
|
+
state.messages.push({ role: openingInterviewer, name: (0, interview_prompts_1.getInterviewerName)(openingInterviewer, customInterviewers), content: fallback });
|
|
491
|
+
}
|
|
492
|
+
// Start interactive loop
|
|
493
|
+
const rl = readline_1.default.createInterface({
|
|
494
|
+
input: process.stdin,
|
|
495
|
+
output: process.stdout,
|
|
496
|
+
prompt: chalk_1.default.green('你 ❯ '),
|
|
497
|
+
});
|
|
498
|
+
rl.prompt();
|
|
499
|
+
rl.on('line', async (input) => {
|
|
500
|
+
const trimmed = input.trim();
|
|
501
|
+
if (!trimmed) {
|
|
502
|
+
rl.prompt();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// Handle commands
|
|
506
|
+
if (trimmed === '/quit' || trimmed === '/exit') {
|
|
507
|
+
state.finished = true;
|
|
508
|
+
renderEnding(state);
|
|
509
|
+
rl.close();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (trimmed === '/status') {
|
|
513
|
+
renderStatusBar(state);
|
|
514
|
+
rl.prompt();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (trimmed === '/help') {
|
|
518
|
+
console.log();
|
|
519
|
+
console.log(chalk_1.default.bold('面试命令:'));
|
|
520
|
+
console.log(chalk_1.default.gray('─').repeat(40));
|
|
521
|
+
console.log(' /status 查看压力/自信状态');
|
|
522
|
+
console.log(' /quit 放弃面试');
|
|
523
|
+
console.log();
|
|
524
|
+
rl.prompt();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
// Validate input
|
|
528
|
+
const validation = validateAnswer(trimmed);
|
|
529
|
+
if (!validation.valid) {
|
|
530
|
+
console.log(chalk_1.default.red(` ⚠ ${validation.error}`));
|
|
531
|
+
rl.prompt();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
// Add user answer
|
|
535
|
+
state.messages.push({ role: 'user', name: '你', content: trimmed });
|
|
536
|
+
// Analyze answer quality
|
|
537
|
+
const analysis = (0, interview_prompts_1.analyzeAnswer)(trimmed);
|
|
538
|
+
state.stress = Math.max(0, Math.min(100, state.stress + analysis.stressChange));
|
|
539
|
+
state.confidence = Math.max(0, Math.min(100, state.confidence + analysis.confidenceChange));
|
|
540
|
+
// Show answer quality badge
|
|
541
|
+
const qualityLabel = QUALITY_LABELS[analysis.quality] || analysis.quality;
|
|
542
|
+
console.log(chalk_1.default.gray(` 回答质量: ${qualityLabel} | 压力 ${analysis.stressChange >= 0 ? '+' : ''}${analysis.stressChange} 自信 ${analysis.confidenceChange >= 0 ? '+' : ''}${analysis.confidenceChange}`));
|
|
543
|
+
// Check if stress hit 100
|
|
544
|
+
if (state.stress >= 100) {
|
|
545
|
+
state.finished = true;
|
|
546
|
+
renderEnding(state);
|
|
547
|
+
rl.close();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
// Maybe trigger random event
|
|
551
|
+
if (Math.random() < 0.15) {
|
|
552
|
+
const event = interview_prompts_1.INTERVIEW_EVENTS[Math.floor(Math.random() * interview_prompts_1.INTERVIEW_EVENTS.length)];
|
|
553
|
+
renderEvent(event.text);
|
|
554
|
+
state.stress = Math.max(0, Math.min(100, state.stress + event.stressChange));
|
|
555
|
+
state.confidence = Math.max(0, Math.min(100, state.confidence + event.confidenceChange));
|
|
556
|
+
if (state.stress >= 100) {
|
|
557
|
+
state.finished = true;
|
|
558
|
+
renderEnding(state);
|
|
559
|
+
rl.close();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// Select which interviewers respond this round
|
|
564
|
+
state.round++;
|
|
565
|
+
const respondents = selectInterviewers(interviewers, state.round, severity);
|
|
566
|
+
// Build context
|
|
567
|
+
const historyMessages = state.messages.slice(-8).map(m => ({
|
|
568
|
+
role: 'user',
|
|
569
|
+
content: m.role === 'user' ? m.content : `(${m.name}说:"${m.content}")`,
|
|
570
|
+
}));
|
|
571
|
+
const interviewSpinner = (0, ora_1.default)({ text: '面试官正在思考...', spinner: 'dots' }).start();
|
|
572
|
+
const respondentResults = [];
|
|
573
|
+
for (const role of respondents) {
|
|
574
|
+
const systemPrompt = (0, interview_prompts_1.getInterviewPrompt)(role, position, severity, state.round, state.totalRounds, state.stress, interviewers, state.customInterviewers, state.candidateProfile);
|
|
575
|
+
const prevSpeech = respondentResults.map(r => `${r.name}说:"${r.content}"`).join('\n');
|
|
576
|
+
const contextWithPrev = [
|
|
577
|
+
...historyMessages,
|
|
578
|
+
...(prevSpeech ? [{ role: 'user', content: `(其他面试官的发言:\n${prevSpeech})` }] : []),
|
|
579
|
+
];
|
|
580
|
+
const messages = [
|
|
581
|
+
{ role: 'system', content: systemPrompt },
|
|
582
|
+
...contextWithPrev,
|
|
583
|
+
];
|
|
584
|
+
try {
|
|
585
|
+
const rawReply = await llm.chat(messages);
|
|
586
|
+
const reply = cleanInterviewResponse(rawReply, role, state.customInterviewers);
|
|
587
|
+
// Interviewer mood affects stress
|
|
588
|
+
const mood = getInterviewerMood(reply);
|
|
589
|
+
const moodStress = (0, interview_prompts_1.analyzeInterviewerMood)(reply);
|
|
590
|
+
state.stress = Math.max(0, Math.min(100, state.stress + moodStress));
|
|
591
|
+
respondentResults.push({
|
|
592
|
+
role,
|
|
593
|
+
name: (0, interview_prompts_1.getInterviewerName)(role, state.customInterviewers),
|
|
594
|
+
content: reply,
|
|
595
|
+
mood,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
600
|
+
// Content filter fallback
|
|
601
|
+
if (errMsg.includes('sensitive') || errMsg.includes('content') || errMsg.includes('400')) {
|
|
602
|
+
const fallbackContent = '请注意你的措辞,我们是正式面试。回到正题吧。';
|
|
603
|
+
respondentResults.push({
|
|
604
|
+
role,
|
|
605
|
+
name: (0, interview_prompts_1.getInterviewerName)(role, state.customInterviewers),
|
|
606
|
+
content: fallbackContent,
|
|
607
|
+
mood: 'cold',
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
logger_1.logger.warning(`${interview_prompts_1.INTERVIEWER_NAMES[role]} 回复失败`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
interviewSpinner.stop();
|
|
616
|
+
if (respondentResults.length === 0) {
|
|
617
|
+
console.log(chalk_1.default.red(' 面试官暂时没有回应,请检查 API 配置'));
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
// Show updated status
|
|
621
|
+
renderStatusBar(state);
|
|
622
|
+
for (const result of respondentResults) {
|
|
623
|
+
renderInterviewerMessage(result.role, result.content, result.mood, state.customInterviewers);
|
|
624
|
+
state.messages.push({
|
|
625
|
+
role: result.role,
|
|
626
|
+
name: result.name,
|
|
627
|
+
content: result.content,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Check end conditions
|
|
632
|
+
if (state.stress >= 100 || state.round >= state.totalRounds) {
|
|
633
|
+
state.finished = true;
|
|
634
|
+
renderEnding(state);
|
|
635
|
+
rl.close();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
rl.prompt();
|
|
639
|
+
});
|
|
640
|
+
rl.on('close', () => {
|
|
641
|
+
if (!state.finished) {
|
|
642
|
+
renderEnding(state);
|
|
643
|
+
}
|
|
644
|
+
console.log();
|
|
645
|
+
logger_1.logger.info('面试结束,再见!');
|
|
646
|
+
process.exit(0);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
if (error.message?.includes('cancelled') || error.message?.includes('User force closed')) {
|
|
651
|
+
console.log();
|
|
652
|
+
logger_1.logger.info('已取消');
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
logger_1.logger.error(error instanceof Error ? error.message : String(error));
|
|
656
|
+
process.exit(1);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
return command;
|
|
660
|
+
}
|