offerguard-mcp-server 0.3.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -98
- package/THIRD_PARTY_NOTICES.txt +2079 -0
- package/build-manifest.json +395 -0
- package/dist/http.mjs +47562 -0
- package/dist/stdio.mjs +21647 -0
- package/package.json +14 -17
- package/src/decision-engine.js +336 -0
- package/src/mcp-server.js +115 -0
- package/src/offerguard-tools.js +6 -476
- package/src/server.js +4 -83
- package/src/stdio.js +1 -1
package/src/offerguard-tools.js
CHANGED
|
@@ -1,476 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const RISK_TERMS = [
|
|
9
|
-
{
|
|
10
|
-
term: "无薪试岗",
|
|
11
|
-
severity: "critical",
|
|
12
|
-
category: "合规红线",
|
|
13
|
-
reason: "试岗或实习期间不支付报酬,学生应优先拒绝或要求书面说明。",
|
|
14
|
-
aliases: ["试岗无薪", "无薪实习", "不发薪", "不支付薪资", "unpaid trial", "unpaid internship"]
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
term: "押金",
|
|
18
|
-
severity: "critical",
|
|
19
|
-
category: "合规红线",
|
|
20
|
-
reason: "要求学生缴纳押金、保证金或类似费用属于高风险信号。",
|
|
21
|
-
aliases: ["保证金", "服装费", "工牌费", "设备押金", "入职收费", "deposit", "security deposit"]
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
term: "培训贷",
|
|
25
|
-
severity: "critical",
|
|
26
|
-
category: "合规红线",
|
|
27
|
-
reason: "把入职、培训或实习机会与贷款绑定,存在明显求职风险。",
|
|
28
|
-
aliases: ["贷款培训", "分期培训", "培训分期", "training loan"]
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
term: "薪资面议",
|
|
32
|
-
severity: "medium",
|
|
33
|
-
category: "薪资透明",
|
|
34
|
-
reason: "薪资结构不透明,面试前需要确认日薪、发放周期和绩效占比。",
|
|
35
|
-
aliases: ["待遇面议", "薪酬面议", "工资面议", "salary negotiable", "pay negotiable"]
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
term: "弹性工作",
|
|
39
|
-
severity: "medium",
|
|
40
|
-
category: "工作强度",
|
|
41
|
-
reason: "弹性工作可能掩盖加班或随时响应要求,需要核实边界。",
|
|
42
|
-
aliases: ["弹性上班", "时间灵活", "flexible work", "flexible schedule"]
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
term: "抗压强",
|
|
46
|
-
severity: "medium",
|
|
47
|
-
category: "工作强度",
|
|
48
|
-
reason: "强调抗压通常意味着强节奏或高不确定性任务。",
|
|
49
|
-
aliases: ["抗压能力强", "能吃苦", "高压环境", "stress resistant", "fast pace"]
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
term: "加班",
|
|
53
|
-
severity: "medium",
|
|
54
|
-
category: "工作强度",
|
|
55
|
-
reason: "需要确认加班频率、调休、补贴以及是否影响课程安排。",
|
|
56
|
-
aliases: ["晚间在线", "周末响应", "随叫随到", "996", "大小周", "overtime", "on call"]
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
term: "转正不明",
|
|
60
|
-
severity: "medium",
|
|
61
|
-
category: "转正路径",
|
|
62
|
-
reason: "转正条件不清晰会影响学生对机会成本的判断。",
|
|
63
|
-
aliases: ["表现优秀可转正", "有转正机会", "转正另议", "possible conversion", "conversion chance"]
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
term: "职责泛化",
|
|
67
|
-
severity: "medium",
|
|
68
|
-
category: "岗位边界",
|
|
69
|
-
reason: "职责过泛会降低成长确定性,也容易落入杂活型实习。",
|
|
70
|
-
aliases: ["其他临时工作", "完成领导安排", "综合事务", "多任务协同", "other temporary tasks"]
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
term: "作品归属公司",
|
|
74
|
-
severity: "high",
|
|
75
|
-
category: "知识产权",
|
|
76
|
-
reason: "作品、代码或账号归属不清会影响简历展示和后续作品集使用。",
|
|
77
|
-
aliases: ["作品版权归公司", "代码归公司", "账号归公司", "不得对外展示", "IP ownership", "portfolio not allowed"]
|
|
78
|
-
}
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
const COMPLIANCE_RULES = [
|
|
82
|
-
{
|
|
83
|
-
id: "unpaid_trial",
|
|
84
|
-
label: "无薪试岗/无薪实习",
|
|
85
|
-
severity: "critical",
|
|
86
|
-
patterns: ["无薪试岗", "试岗无薪", "无薪实习", "不发薪", "不支付薪资", "unpaid trial", "unpaid internship"],
|
|
87
|
-
action: "建议拒绝,除非对方提供合法、明确、书面的报酬与协议安排。"
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
id: "deposit_or_fee",
|
|
91
|
-
label: "押金/保证金/入职收费",
|
|
92
|
-
severity: "critical",
|
|
93
|
-
patterns: ["押金", "保证金", "服装费", "工牌费", "设备押金", "入职收费", "deposit", "security deposit"],
|
|
94
|
-
action: "建议拒绝,不应为获得实习机会提前缴费。"
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: "training_loan",
|
|
98
|
-
label: "培训贷/分期培训",
|
|
99
|
-
severity: "critical",
|
|
100
|
-
patterns: ["培训贷", "贷款培训", "分期培训", "培训分期", "training loan"],
|
|
101
|
-
action: "建议拒绝,并保留沟通记录。"
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
id: "work_intensity",
|
|
105
|
-
label: "工作时长和响应边界不清",
|
|
106
|
-
severity: "high",
|
|
107
|
-
patterns: ["996", "大小周", "随叫随到", "晚间在线", "周末响应", "高强度加班", "on call", "long overtime"],
|
|
108
|
-
action: "必须在面试中确认工作时长、调休和补贴规则。"
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
id: "ip_ownership",
|
|
112
|
-
label: "作品/代码/账号归属不清",
|
|
113
|
-
severity: "high",
|
|
114
|
-
patterns: ["作品归属公司", "作品版权归公司", "代码归公司", "账号归公司", "不得对外展示", "IP ownership", "portfolio not allowed"],
|
|
115
|
-
action: "必须要求写入协议,确认可否用于简历、作品集或面试展示。"
|
|
116
|
-
}
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
const ROLE_KEYWORDS = {
|
|
120
|
-
media: ["新媒体", "运营", "公众号", "小红书", "短视频", "活动策划", "content", "operation", "xiaohongshu"],
|
|
121
|
-
backend: ["Java", "后端", "接口", "MySQL", "Redis", "计算机", "backend", "API"],
|
|
122
|
-
product: ["产品", "需求", "原型", "用户研究", "数据分析", "product", "PRD", "prototype"],
|
|
123
|
-
design: ["设计", "Figma", "视觉", "作品集", "交互", "design"]
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
function normalizeText(value) {
|
|
127
|
-
if (!value) return "";
|
|
128
|
-
if (typeof value === "string") return value;
|
|
129
|
-
return JSON.stringify(value, null, 2);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function getPrimaryText(input) {
|
|
133
|
-
return normalizeText(
|
|
134
|
-
input?.AGENT_USER_INPUT ??
|
|
135
|
-
input?.raw_input ??
|
|
136
|
-
input?.rawInput ??
|
|
137
|
-
input?.jdText ??
|
|
138
|
-
input?.text ??
|
|
139
|
-
input
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function clamp(score) {
|
|
144
|
-
return Math.max(0, Math.min(100, Math.round(score)));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function includesAny(text, patterns) {
|
|
148
|
-
const lower = text.toLowerCase();
|
|
149
|
-
return patterns.some(pattern => lower.includes(String(pattern).toLowerCase()));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function extractEvidence(text, pattern) {
|
|
153
|
-
const lower = text.toLowerCase();
|
|
154
|
-
const needle = String(pattern).toLowerCase();
|
|
155
|
-
const index = lower.indexOf(needle);
|
|
156
|
-
if (index < 0) return "";
|
|
157
|
-
const start = Math.max(0, index - 24);
|
|
158
|
-
const end = Math.min(text.length, index + pattern.length + 36);
|
|
159
|
-
return text.slice(start, end).replace(/\s+/g, " ").trim();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function findFirstAlias(text, term) {
|
|
163
|
-
const patterns = [term.term, ...term.aliases];
|
|
164
|
-
return patterns.find(pattern => includesAny(text, [pattern]));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function countBySeverity(items) {
|
|
168
|
-
return items.reduce(
|
|
169
|
-
(acc, item) => {
|
|
170
|
-
acc[item.severity] += 1;
|
|
171
|
-
return acc;
|
|
172
|
-
},
|
|
173
|
-
{ low: 0, medium: 0, high: 0, critical: 0 }
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function highestSeverity(items) {
|
|
178
|
-
if (!items.length) return "none";
|
|
179
|
-
return items.reduce((highest, item) => {
|
|
180
|
-
return SEVERITY_WEIGHT[item.severity] > SEVERITY_WEIGHT[highest]
|
|
181
|
-
? item.severity
|
|
182
|
-
: highest;
|
|
183
|
-
}, "low");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function salaryIsExplicit(text) {
|
|
187
|
-
return /(\d{2,4}\s*[-~至到]\s*\d{2,4}\s*元?\/?(天|日|月)?)|(\d{2,4}\s*元\/?(天|日))|(\d{3,5}\s*\/月)|(\d{2,4}\s*rmb)/i.test(text);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function hasAgreement(text) {
|
|
191
|
-
return includesAny(text, ["实习协议", "三方协议", "劳动合同", "书面协议", "电子签", "written agreement", "contract"]);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function hasMentor(text) {
|
|
195
|
-
return includesAny(text, ["导师", "带教", "mentor", "主管指导", "代码评审", "培训体系"]);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function hasConversionPath(text) {
|
|
199
|
-
return includesAny(text, ["转正标准", "转正流程", "考核指标", "留用", "转正名额", "conversion criteria"]);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function roleMatchBonus(jdText, studentText) {
|
|
203
|
-
const keywordGroups = Object.values(ROLE_KEYWORDS);
|
|
204
|
-
return keywordGroups.some(keywords =>
|
|
205
|
-
keywords.some(word => includesAny(jdText, [word])) &&
|
|
206
|
-
keywords.some(word => includesAny(studentText, [word]))
|
|
207
|
-
)
|
|
208
|
-
? 10
|
|
209
|
-
: 0;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function matchRiskTerms(input) {
|
|
213
|
-
const jdText = getPrimaryText(input);
|
|
214
|
-
const matches = RISK_TERMS.map(term => {
|
|
215
|
-
const matched = findFirstAlias(jdText, term);
|
|
216
|
-
if (!matched) return null;
|
|
217
|
-
return {
|
|
218
|
-
term: term.term,
|
|
219
|
-
matched_text: matched,
|
|
220
|
-
severity: term.severity,
|
|
221
|
-
category: term.category,
|
|
222
|
-
evidence: extractEvidence(jdText, matched),
|
|
223
|
-
trigger_reason: term.reason
|
|
224
|
-
};
|
|
225
|
-
}).filter(Boolean);
|
|
226
|
-
|
|
227
|
-
const severity_counts = countBySeverity(matches);
|
|
228
|
-
const penalty = matches.reduce((sum, item) => sum + SEVERITY_WEIGHT[item.severity] * 4, 0);
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
tool: "risk_term_matcher",
|
|
232
|
-
version: "0.3.0",
|
|
233
|
-
risk_terms: matches,
|
|
234
|
-
summary: {
|
|
235
|
-
total_hits: matches.length,
|
|
236
|
-
severity_counts,
|
|
237
|
-
highest_severity: highestSeverity(matches),
|
|
238
|
-
risk_score_penalty: Math.min(45, penalty)
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export function checkInternshipCompliance(input) {
|
|
244
|
-
const text = [
|
|
245
|
-
getPrimaryText(input),
|
|
246
|
-
input?.offerText,
|
|
247
|
-
input?.extraText
|
|
248
|
-
]
|
|
249
|
-
.map(normalizeText)
|
|
250
|
-
.filter(Boolean)
|
|
251
|
-
.join("\n");
|
|
252
|
-
|
|
253
|
-
const flags = COMPLIANCE_RULES.map(rule => {
|
|
254
|
-
const matched = rule.patterns.find(pattern => includesAny(text, [pattern]));
|
|
255
|
-
if (!matched) return null;
|
|
256
|
-
return {
|
|
257
|
-
id: rule.id,
|
|
258
|
-
label: rule.label,
|
|
259
|
-
severity: rule.severity,
|
|
260
|
-
matched_text: matched,
|
|
261
|
-
evidence: extractEvidence(text, matched),
|
|
262
|
-
recommended_action: rule.action
|
|
263
|
-
};
|
|
264
|
-
}).filter(Boolean);
|
|
265
|
-
|
|
266
|
-
const missing_items = [];
|
|
267
|
-
if (!salaryIsExplicit(text) || includesAny(text, ["薪资面议", "待遇面议", "工资面议", "salary negotiable", "pay negotiable"])) {
|
|
268
|
-
missing_items.push({
|
|
269
|
-
id: "salary_structure",
|
|
270
|
-
label: "薪资结构/发放周期",
|
|
271
|
-
severity: "medium",
|
|
272
|
-
recommended_question: "请问日薪或月薪、发放时间、绩效占比和是否有扣款规则分别是什么?"
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
if (!hasAgreement(text)) {
|
|
276
|
-
missing_items.push({
|
|
277
|
-
id: "written_agreement",
|
|
278
|
-
label: "书面实习协议",
|
|
279
|
-
severity: "high",
|
|
280
|
-
recommended_question: "入职前是否签署书面实习协议,协议中是否包含薪资、工时、保险和作品归属?"
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
if (!hasConversionPath(text) && includesAny(text, ["转正", "留用", "conversion"])) {
|
|
284
|
-
missing_items.push({
|
|
285
|
-
id: "conversion_criteria",
|
|
286
|
-
label: "转正标准",
|
|
287
|
-
severity: "medium",
|
|
288
|
-
recommended_question: "转正考核指标、名额、周期和审批人分别是什么?"
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const red_lines = flags.filter(flag => flag.severity === "critical");
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
tool: "internship_compliance_checker",
|
|
296
|
-
version: "0.3.0",
|
|
297
|
-
compliance_flags: flags,
|
|
298
|
-
red_lines,
|
|
299
|
-
missing_items,
|
|
300
|
-
summary: {
|
|
301
|
-
red_line_count: red_lines.length,
|
|
302
|
-
high_risk_count: flags.filter(flag => flag.severity === "high").length,
|
|
303
|
-
missing_item_count: missing_items.length,
|
|
304
|
-
can_generate_report: red_lines.length === 0
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export function calculateOfferScore(input) {
|
|
310
|
-
const jdText = normalizeText(input?.jdText ?? input?.jobProfile ?? getPrimaryText(input));
|
|
311
|
-
const studentText = normalizeText(input?.studentProfile ?? input?.AGENT_USER_INPUT ?? input?.raw_input ?? "");
|
|
312
|
-
const riskResult = input?.riskTermResult?.risk_terms ? input.riskTermResult : matchRiskTerms({ jdText });
|
|
313
|
-
const complianceResult = input?.complianceResult?.summary ? input.complianceResult : checkInternshipCompliance({ jdText });
|
|
314
|
-
|
|
315
|
-
const riskTerms = riskResult.risk_terms ?? [];
|
|
316
|
-
const flags = complianceResult.compliance_flags ?? [];
|
|
317
|
-
const missingItems = complianceResult.missing_items ?? [];
|
|
318
|
-
const criticalCount = complianceResult.summary?.red_line_count ?? 0;
|
|
319
|
-
const highCount = flags.filter(flag => flag.severity === "high").length;
|
|
320
|
-
const mediumCount = [
|
|
321
|
-
...riskTerms.filter(item => item.severity === "medium"),
|
|
322
|
-
...missingItems.filter(item => item.severity === "medium")
|
|
323
|
-
].length;
|
|
324
|
-
|
|
325
|
-
const explicitSalary = salaryIsExplicit(jdText);
|
|
326
|
-
const mentor = hasMentor(jdText);
|
|
327
|
-
const conversionPath = hasConversionPath(jdText);
|
|
328
|
-
const projectValue = includesAny(jdText, ["上线", "核心项目", "真实项目", "可量化", "作品", "数据复盘", "portfolio", "project"]);
|
|
329
|
-
const repetitive = includesAny(jdText, ["资料整理", "简单重复", "打杂", "行政杂务", "搬运", "repetitive"]);
|
|
330
|
-
const overtime = includesAny(jdText, ["996", "大小周", "随叫随到", "晚间在线", "周末响应", "加班", "overtime", "on call"]);
|
|
331
|
-
const wantsStableHours = includesAny(studentText, ["不希望长期夜间", "不接受加班", "不希望长期高强度", "课程", "考试", "stable hours"]);
|
|
332
|
-
|
|
333
|
-
const dimensions = [
|
|
334
|
-
{
|
|
335
|
-
key: "role_match",
|
|
336
|
-
name: "岗位匹配",
|
|
337
|
-
score: clamp(68 + roleMatchBonus(jdText, studentText) - (repetitive ? 8 : 0)),
|
|
338
|
-
rationale: "根据岗位关键词与学生经历方向的重合度计算。"
|
|
339
|
-
},
|
|
340
|
-
{
|
|
341
|
-
key: "growth_value",
|
|
342
|
-
name: "成长价值",
|
|
343
|
-
score: clamp(65 + (mentor ? 12 : 0) + (projectValue ? 10 : 0) - (repetitive ? 18 : 0)),
|
|
344
|
-
rationale: "导师、真实项目和可沉淀产出会提升成长价值。"
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
key: "salary_transparency",
|
|
348
|
-
name: "薪资透明",
|
|
349
|
-
score: clamp(58 + (explicitSalary ? 20 : -12) - (includesAny(jdText, ["薪资面议", "待遇面议", "salary negotiable"]) ? 15 : 0)),
|
|
350
|
-
rationale: "薪资金额、发放周期和绩效占比越明确,透明度越高。"
|
|
351
|
-
},
|
|
352
|
-
{
|
|
353
|
-
key: "work_intensity",
|
|
354
|
-
name: "工作强度",
|
|
355
|
-
score: clamp(78 - (overtime ? 25 : 0) - (wantsStableHours && overtime ? 15 : 0)),
|
|
356
|
-
rationale: "加班、随叫随到和学生时间偏好冲突会拉低该项。"
|
|
357
|
-
},
|
|
358
|
-
{
|
|
359
|
-
key: "compliance_safety",
|
|
360
|
-
name: "合规安全",
|
|
361
|
-
score: clamp(86 - criticalCount * 55 - highCount * 22 - mediumCount * 6 - missingItems.length * 5),
|
|
362
|
-
rationale: "无薪试岗、押金、培训贷等红线优先级最高。"
|
|
363
|
-
},
|
|
364
|
-
{
|
|
365
|
-
key: "conversion_clarity",
|
|
366
|
-
name: "转正清晰",
|
|
367
|
-
score: clamp(62 + (conversionPath ? 20 : 0) - (includesAny(jdText, ["转正另议", "表现优秀可转正", "有转正机会", "possible conversion"]) ? 15 : 0)),
|
|
368
|
-
rationale: "有量化转正标准和流程时分数更高。"
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
key: "resume_value",
|
|
372
|
-
name: "简历增益",
|
|
373
|
-
score: clamp(66 + (projectValue ? 15 : 0) + (mentor ? 5 : 0) - (repetitive ? 20 : 0)),
|
|
374
|
-
rationale: "能否形成项目、数据、作品或证明材料决定简历增益。"
|
|
375
|
-
}
|
|
376
|
-
];
|
|
377
|
-
|
|
378
|
-
const overall_score = clamp(dimensions.reduce((sum, item) => sum + item.score, 0) / dimensions.length);
|
|
379
|
-
let risk_level = "low";
|
|
380
|
-
let route = "advance";
|
|
381
|
-
let one_sentence_reason = "岗位信息较完整,暂未发现高风险红线,可继续推进并验证细节。";
|
|
382
|
-
|
|
383
|
-
if (criticalCount > 0) {
|
|
384
|
-
risk_level = "critical";
|
|
385
|
-
route = "reject";
|
|
386
|
-
one_sentence_reason = "命中实习合规红线,建议停止推进并保留沟通证据。";
|
|
387
|
-
} else if (highCount > 0 || riskTerms.some(item => item.severity === "high")) {
|
|
388
|
-
risk_level = "high";
|
|
389
|
-
route = "verify";
|
|
390
|
-
one_sentence_reason = "存在高风险信号,必须通过面试追问和书面材料验证后再决定。";
|
|
391
|
-
} else if (overall_score < 55) {
|
|
392
|
-
risk_level = "high";
|
|
393
|
-
route = "reject";
|
|
394
|
-
one_sentence_reason = "综合分较低且成长、合规或透明度不足,不建议优先选择。";
|
|
395
|
-
} else if (mediumCount >= 2 || missingItems.length >= 2 || overall_score < 73) {
|
|
396
|
-
risk_level = "medium";
|
|
397
|
-
route = explicitSalary ? "verify" : "negotiate";
|
|
398
|
-
one_sentence_reason = "信息仍有缺口或中风险较多,建议先验证薪资、工时、转正和协议。";
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return {
|
|
402
|
-
tool: "offer_score_calculator",
|
|
403
|
-
version: "0.3.0",
|
|
404
|
-
overall_score,
|
|
405
|
-
risk_level,
|
|
406
|
-
route,
|
|
407
|
-
route_marker: `ROUTE: ${route}`,
|
|
408
|
-
route_real_marker: `ROUTE_REAL: ${route}`,
|
|
409
|
-
one_sentence_reason,
|
|
410
|
-
dimensions,
|
|
411
|
-
decision_rules_triggered: {
|
|
412
|
-
critical_compliance_first: criticalCount > 0,
|
|
413
|
-
high_risk_requires_verification: highCount > 0 || riskTerms.some(item => item.severity === "high"),
|
|
414
|
-
missing_information_requires_followup: missingItems.length >= 2
|
|
415
|
-
},
|
|
416
|
-
required_followup_questions: [
|
|
417
|
-
...missingItems.map(item => item.recommended_question),
|
|
418
|
-
...(overtime ? ["请问实习生平均每周工作时长、加班频率、调休和补贴规则分别是什么?"] : []),
|
|
419
|
-
...(projectValue ? [] : ["这个岗位三个月内可以沉淀哪些项目、数据或作品证明?"])
|
|
420
|
-
].slice(0, 6)
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export function auditOfferGuardInput(input) {
|
|
425
|
-
const raw_input = getPrimaryText(input);
|
|
426
|
-
const risk = matchRiskTerms({ jdText: raw_input });
|
|
427
|
-
const compliance = checkInternshipCompliance({ jdText: raw_input, extraText: raw_input });
|
|
428
|
-
const score = calculateOfferScore({
|
|
429
|
-
jdText: raw_input,
|
|
430
|
-
studentProfile: raw_input,
|
|
431
|
-
riskTermResult: risk,
|
|
432
|
-
complianceResult: compliance
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
const mustAsk = [
|
|
436
|
-
...score.required_followup_questions,
|
|
437
|
-
"岗位的核心工作占比、导师机制和前三周交付物分别是什么?",
|
|
438
|
-
"Offer 或实习协议中能否书面确认薪资、工时、到岗、作品归属和离职规则?"
|
|
439
|
-
].slice(0, 6);
|
|
440
|
-
|
|
441
|
-
return {
|
|
442
|
-
tool: "offerguard_audit",
|
|
443
|
-
version: "0.3.0",
|
|
444
|
-
mcp_ok: true,
|
|
445
|
-
raw_input_length: raw_input.length,
|
|
446
|
-
route: score.route,
|
|
447
|
-
risk_level: score.risk_level,
|
|
448
|
-
overall_score: score.overall_score,
|
|
449
|
-
route_marker: score.route_marker,
|
|
450
|
-
route_real_marker: score.route_real_marker,
|
|
451
|
-
one_sentence_reason: score.one_sentence_reason,
|
|
452
|
-
risk_terms: risk.risk_terms,
|
|
453
|
-
compliance_red_lines: compliance.red_lines,
|
|
454
|
-
compliance_missing_items: compliance.missing_items,
|
|
455
|
-
dimensions: score.dimensions,
|
|
456
|
-
required_followup_questions: mustAsk,
|
|
457
|
-
negotiation_cards: [
|
|
458
|
-
"您好,我想确认一下这个岗位的薪资结构、发放周期和是否有绩效/扣款规则,方便我判断是否匹配实习安排。",
|
|
459
|
-
"我希望提前了解实习协议中对工作时间、加班调休、作品归属和实习证明的约定,是否可以在入职前书面确认?",
|
|
460
|
-
"如果后续有转正机会,能否请您说明考核指标、时间节点和审批流程?"
|
|
461
|
-
],
|
|
462
|
-
accept_conditions: [
|
|
463
|
-
"薪资、发放周期、到岗时间和工时边界可以书面确认。",
|
|
464
|
-
"没有无薪试岗、押金、培训贷等合规红线。",
|
|
465
|
-
"导师机制、核心任务和可沉淀产出明确。",
|
|
466
|
-
"转正或实习证明规则清晰,不以口头承诺替代书面约定。"
|
|
467
|
-
],
|
|
468
|
-
mcp_audit_summary: {
|
|
469
|
-
risk_hit_count: risk.summary.total_hits,
|
|
470
|
-
red_line_count: compliance.summary.red_line_count,
|
|
471
|
-
missing_item_count: compliance.summary.missing_item_count,
|
|
472
|
-
score_route: score.route,
|
|
473
|
-
score_risk_level: score.risk_level
|
|
474
|
-
}
|
|
475
|
-
};
|
|
476
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
auditOfferGuardInput,
|
|
3
|
+
calculateOfferScore,
|
|
4
|
+
checkInternshipCompliance,
|
|
5
|
+
matchRiskTerms
|
|
6
|
+
} from "./decision-engine.js";
|
package/src/server.js
CHANGED
|
@@ -1,94 +1,15 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
3
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
4
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
6
5
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
7
6
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
calculateOfferScore,
|
|
12
|
-
checkInternshipCompliance,
|
|
13
|
-
matchRiskTerms
|
|
14
|
-
} from "./offerguard-tools.js";
|
|
7
|
+
import { TOOL_VERSION } from "./decision-engine.js";
|
|
8
|
+
import { createOfferGuardMcpServer } from "./mcp-server.js";
|
|
9
|
+
export { createOfferGuardMcpServer } from "./mcp-server.js";
|
|
15
10
|
|
|
16
11
|
const SERVICE_NAME = "OfferGuardMCP";
|
|
17
|
-
const SERVICE_VERSION =
|
|
18
|
-
|
|
19
|
-
function jsonToolResult(result) {
|
|
20
|
-
return {
|
|
21
|
-
content: [
|
|
22
|
-
{
|
|
23
|
-
type: "text",
|
|
24
|
-
text: JSON.stringify(result, null, 2)
|
|
25
|
-
}
|
|
26
|
-
],
|
|
27
|
-
structuredContent: result
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function createOfferGuardMcpServer() {
|
|
32
|
-
const server = new McpServer({
|
|
33
|
-
name: SERVICE_NAME,
|
|
34
|
-
version: SERVICE_VERSION
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
server.registerTool(
|
|
38
|
-
"offerguard_audit",
|
|
39
|
-
{
|
|
40
|
-
description: "All-in-one OfferGuard audit for XFYun workflow MCP nodes. It accepts raw_input and returns route, scores, risk terms, compliance flags, negotiation cards, and MCP audit summary.",
|
|
41
|
-
inputSchema: {
|
|
42
|
-
raw_input: z
|
|
43
|
-
.string()
|
|
44
|
-
.min(1)
|
|
45
|
-
.describe("Raw user input from the Agent workflow, including JD, resume/profile, preferences, and risk tolerance. Bind this to Start / AGENT_USER_INPUT in XFYun.")
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
async input => jsonToolResult(auditOfferGuardInput(input))
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
server.registerTool(
|
|
52
|
-
"risk_term_matcher",
|
|
53
|
-
{
|
|
54
|
-
description: "Match internship JD risk terms and return severity, source evidence, and trigger reasons.",
|
|
55
|
-
inputSchema: {
|
|
56
|
-
jdText: z.string().min(1).describe("Raw internship JD or offer text.")
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
async input => jsonToolResult(matchRiskTerms(input))
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
server.registerTool(
|
|
63
|
-
"internship_compliance_checker",
|
|
64
|
-
{
|
|
65
|
-
description: "Check internship compliance red lines such as unpaid trials, deposits, training loans, work hours, salary payment, agreements, and IP ownership.",
|
|
66
|
-
inputSchema: {
|
|
67
|
-
jdText: z.string().min(1).describe("Raw internship JD text."),
|
|
68
|
-
offerText: z.string().optional().describe("Optional offer or HR message text."),
|
|
69
|
-
extraText: z.string().optional().describe("Optional extra context from the student.")
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
async input => jsonToolResult(checkInternshipCompliance(input))
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
server.registerTool(
|
|
76
|
-
"offer_score_calculator",
|
|
77
|
-
{
|
|
78
|
-
description: "Calculate OfferGuard seven-dimension scores, overall risk level, and recommendation route.",
|
|
79
|
-
inputSchema: {
|
|
80
|
-
jdText: z.string().min(1).describe("Raw internship JD text."),
|
|
81
|
-
studentProfile: z.union([z.string(), z.record(z.string(), z.any())]).describe("Student profile, resume summary, preferences, and risk tolerance."),
|
|
82
|
-
jobProfile: z.union([z.string(), z.record(z.string(), z.any())]).optional().describe("Optional parsed job profile."),
|
|
83
|
-
riskTermResult: z.record(z.string(), z.any()).optional().describe("Optional output from risk_term_matcher."),
|
|
84
|
-
complianceResult: z.record(z.string(), z.any()).optional().describe("Optional output from internship_compliance_checker.")
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
async input => jsonToolResult(calculateOfferScore(input))
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
return server;
|
|
91
|
-
}
|
|
12
|
+
const SERVICE_VERSION = TOOL_VERSION;
|
|
92
13
|
|
|
93
14
|
export function createApp({ host = process.env.HOST ?? "127.0.0.1" } = {}) {
|
|
94
15
|
const app = createMcpExpressApp({ host });
|
package/src/stdio.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { createOfferGuardMcpServer } from "./server.js";
|
|
4
|
+
import { createOfferGuardMcpServer } from "./mcp-server.js";
|
|
5
5
|
|
|
6
6
|
const server = createOfferGuardMcpServer();
|
|
7
7
|
const transport = new StdioServerTransport();
|