offerguard-mcp-server 0.4.0 → 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 -114
- 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 +13 -21
- package/src/decision-engine.js +8 -2
- package/src/mcp-server.js +115 -0
- package/src/server.js +2 -110
- package/src/stdio.js +1 -1
- package/docs/SCORING.md +0 -42
- package/docs/WORKFLOW_INTEGRATION.md +0 -64
- package/scripts/validate-release.js +0 -53
- package/test/decision-engine.test.js +0 -210
- package/test/fixtures.js +0 -10
- package/test/mcp-protocol.test.js +0 -84
- package/workflow/retest-prompts.json +0 -16
package/package.json
CHANGED
|
@@ -1,32 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "offerguard-mcp-server",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"private": false,
|
|
5
|
-
"type": "module",
|
|
3
|
+
"version": "0.4.2",
|
|
6
4
|
"description": "OfferGuard MCP Server for internship offer auditing, compliance checks, scoring, and workflow routing.",
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"test": "node --test test/decision-engine.test.js test/mcp-protocol.test.js",
|
|
11
|
-
"validate": "node scripts/validate-release.js"
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
12
8
|
},
|
|
13
9
|
"bin": {
|
|
14
|
-
"offerguard-mcp-server": "
|
|
10
|
+
"offerguard-mcp-server": "dist/stdio.mjs"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node dist/http.mjs",
|
|
14
|
+
"stdio": "node dist/stdio.mjs"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
+
"dist",
|
|
17
18
|
"src",
|
|
18
19
|
"README.md",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"docs",
|
|
22
|
-
"workflow"
|
|
20
|
+
"THIRD_PARTY_NOTICES.txt",
|
|
21
|
+
"build-manifest.json"
|
|
23
22
|
],
|
|
24
|
-
"dependencies": {
|
|
25
|
-
"@modelcontextprotocol/sdk": "^1.17.5",
|
|
26
|
-
"express": "^5.1.0",
|
|
27
|
-
"zod": "^3.25.76"
|
|
28
|
-
},
|
|
29
|
-
"engines": {
|
|
30
|
-
"node": ">=20"
|
|
31
|
-
}
|
|
23
|
+
"dependencies": {}
|
|
32
24
|
}
|
package/src/decision-engine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
export const TOOL_VERSION = "0.4.
|
|
4
|
-
const RULE_VERSION = "2026-09-evidence-
|
|
3
|
+
export const TOOL_VERSION = "0.4.2";
|
|
4
|
+
const RULE_VERSION = "2026-09-evidence-2";
|
|
5
5
|
const hash = s => createHash("sha256").update(s).digest("hex").slice(0, 16);
|
|
6
6
|
const text = s => typeof s === "string" ? s.trim() : "";
|
|
7
7
|
const record = (source, value) => ({ id: hash(`${source}:${value}`), source, text: value });
|
|
@@ -83,6 +83,7 @@ function polarity(clause, pattern) {
|
|
|
83
83
|
const m = clause.match(pattern); if (!m) return null;
|
|
84
84
|
const before = clause.slice(0, m.index), after = clause.slice(m.index + m[0].length);
|
|
85
85
|
if (/不接受|不希望|底线|希望有|想要|期望/.test(before)) return "unknown";
|
|
86
|
+
if (/(?:尚未|暂未|还未|还没)(?:提供|明确|说明|确认|给出|披露)/.test(clause)) return "unknown";
|
|
86
87
|
if (/[??]$/.test(clause) || /是否|有没有|未说明|未明确|未确认|未知|不清楚|不清晰|不明|不详|待定|待确认|待验证|可能|不确定|不保证|如果|假如|假设|计划|有望|预计/.test(clause)) return "unknown";
|
|
87
88
|
if (/并非不|不是不|不能不|不得不|不排除|没有说|未承诺|未表示/.test(before)) return "unknown";
|
|
88
89
|
if (/\bnot only\b/i.test(before)) return "unknown";
|
|
@@ -241,6 +242,11 @@ function render(r) {
|
|
|
241
242
|
}
|
|
242
243
|
export function auditOfferGuardInput(input = {}) {
|
|
243
244
|
const start = performance.now();
|
|
245
|
+
validateInput(input);
|
|
246
|
+
const currentText = text(input.raw_input), legacyText = text(input.AGENT_USER_INPUT);
|
|
247
|
+
if (currentText && legacyText && currentText !== legacyText) throw new Error("Conflicting raw_input and AGENT_USER_INPUT; provide only one source message.");
|
|
248
|
+
// Normalize before envelope parsing so legacy bindings retain multi-turn inputs.
|
|
249
|
+
input = { ...input, raw_input: currentText || legacyText, AGENT_USER_INPUT: undefined };
|
|
244
250
|
const envelopeText = text(input.raw_input);
|
|
245
251
|
if (envelopeText.startsWith("{")) {
|
|
246
252
|
let envelope;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { TOOL_VERSION } from "./decision-engine.js";
|
|
4
|
+
import {
|
|
5
|
+
auditOfferGuardInput,
|
|
6
|
+
calculateOfferScore,
|
|
7
|
+
checkInternshipCompliance,
|
|
8
|
+
matchRiskTerms
|
|
9
|
+
} from "./offerguard-tools.js";
|
|
10
|
+
|
|
11
|
+
const SERVICE_NAME = "OfferGuardMCP";
|
|
12
|
+
const SERVICE_VERSION = TOOL_VERSION;
|
|
13
|
+
|
|
14
|
+
const sourceText = z.string().max(40000);
|
|
15
|
+
const sourceRecord = z.object({
|
|
16
|
+
id: z.string().optional(),
|
|
17
|
+
source: z.enum(["jd", "student", "hr", "unattributed"]),
|
|
18
|
+
text: sourceText
|
|
19
|
+
});
|
|
20
|
+
const assessmentInput = {
|
|
21
|
+
raw_input: sourceText.optional().describe("Original labeled JD / student / HR text, or a JSON string envelope with offerguard_schema:1, user_message, structured_input, previous_state and mode. Never invent missing values."),
|
|
22
|
+
AGENT_USER_INPUT: sourceText.optional().describe("Legacy alias of raw_input for existing Agent nodes. If both are provided, they must contain the same text."),
|
|
23
|
+
structured_input: z.object({
|
|
24
|
+
original_input: sourceText.optional().describe("Original current user message. When supplied, extracted source fields must be literal excerpts of it."),
|
|
25
|
+
jd_text: sourceText.optional(),
|
|
26
|
+
student_profile: sourceText.optional(),
|
|
27
|
+
hr_reply: sourceText.optional(),
|
|
28
|
+
offers: z.array(z.object({ id: z.string().min(1).max(40), jd_text: sourceText.min(1), hr_reply: sourceText.optional() })).length(2).optional()
|
|
29
|
+
}).optional().describe("Separate original source documents, not model-computed facts or scores."),
|
|
30
|
+
previous_state: z.object({ schema_version: z.literal(1), records: z.array(sourceRecord).max(120) }).optional()
|
|
31
|
+
.describe("Previous next_state for this conversation and this offer only. Keep private; never share between users."),
|
|
32
|
+
mode: z.enum(["single_check", "negotiation", "interview_prep", "accept_conditions", "full_report"]).optional(),
|
|
33
|
+
new_assessment: z.boolean().optional().describe("True starts a different offer and discards previous_state.")
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function jsonToolResult(result) {
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: JSON.stringify(result, null, 2)
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
structuredContent: result
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function runTool(handler, input) {
|
|
49
|
+
try {
|
|
50
|
+
return jsonToolResult(handler(input));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
...jsonToolResult({ mcp_ok: false, error_code: "INVALID_ASSESSMENT_INPUT", error: error.message,
|
|
54
|
+
overall_score: null, report_text: "MCP 工具输入无效,暂不能生成 OfferGuard 风险报告。" }),
|
|
55
|
+
isError: true
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createOfferGuardMcpServer() {
|
|
61
|
+
const server = new McpServer({
|
|
62
|
+
name: SERVICE_NAME,
|
|
63
|
+
version: SERVICE_VERSION
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
server.registerTool(
|
|
67
|
+
"offerguard_audit",
|
|
68
|
+
{
|
|
69
|
+
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.",
|
|
70
|
+
inputSchema: assessmentInput
|
|
71
|
+
},
|
|
72
|
+
async input => runTool(auditOfferGuardInput, input)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
server.registerTool(
|
|
76
|
+
"risk_term_matcher",
|
|
77
|
+
{
|
|
78
|
+
description: "Match internship JD risk terms and return severity, source evidence, and trigger reasons.",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
jdText: z.string().min(1).describe("Raw internship JD or offer text.")
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
async input => runTool(matchRiskTerms, input)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
server.registerTool(
|
|
87
|
+
"internship_compliance_checker",
|
|
88
|
+
{
|
|
89
|
+
description: "Check internship compliance red lines such as unpaid trials, deposits, training loans, work hours, salary payment, agreements, and IP ownership.",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
jdText: z.string().min(1).describe("Raw internship JD text."),
|
|
92
|
+
offerText: z.string().optional().describe("Optional offer or HR message text."),
|
|
93
|
+
extraText: z.string().optional().describe("Optional extra context from the student.")
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
async input => runTool(checkInternshipCompliance, input)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
server.registerTool(
|
|
100
|
+
"offer_score_calculator",
|
|
101
|
+
{
|
|
102
|
+
description: "Calculate OfferGuard seven-dimension scores, overall risk level, and recommendation route.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
jdText: z.string().min(1).describe("Raw internship JD text."),
|
|
105
|
+
studentProfile: z.union([z.string(), z.record(z.string(), z.any())]).describe("Student profile, resume summary, preferences, and risk tolerance."),
|
|
106
|
+
jobProfile: z.union([z.string(), z.record(z.string(), z.any())]).optional().describe("Optional parsed job profile."),
|
|
107
|
+
riskTermResult: z.record(z.string(), z.any()).optional().describe("Optional output from risk_term_matcher."),
|
|
108
|
+
complianceResult: z.record(z.string(), z.any()).optional().describe("Optional output from internship_compliance_checker.")
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
async input => runTool(calculateOfferScore, input)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return server;
|
|
115
|
+
}
|
package/src/server.js
CHANGED
|
@@ -1,124 +1,16 @@
|
|
|
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 { z } from "zod";
|
|
9
7
|
import { TOOL_VERSION } from "./decision-engine.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
calculateOfferScore,
|
|
13
|
-
checkInternshipCompliance,
|
|
14
|
-
matchRiskTerms
|
|
15
|
-
} from "./offerguard-tools.js";
|
|
8
|
+
import { createOfferGuardMcpServer } from "./mcp-server.js";
|
|
9
|
+
export { createOfferGuardMcpServer } from "./mcp-server.js";
|
|
16
10
|
|
|
17
11
|
const SERVICE_NAME = "OfferGuardMCP";
|
|
18
12
|
const SERVICE_VERSION = TOOL_VERSION;
|
|
19
13
|
|
|
20
|
-
const sourceText = z.string().max(40000);
|
|
21
|
-
const sourceRecord = z.object({
|
|
22
|
-
id: z.string().optional(),
|
|
23
|
-
source: z.enum(["jd", "student", "hr", "unattributed"]),
|
|
24
|
-
text: sourceText
|
|
25
|
-
});
|
|
26
|
-
const assessmentInput = {
|
|
27
|
-
raw_input: sourceText.optional().describe("Original labeled JD / student / HR text, or a JSON string envelope with offerguard_schema:1, user_message, structured_input, previous_state and mode. Never invent missing values."),
|
|
28
|
-
structured_input: z.object({
|
|
29
|
-
original_input: sourceText.optional().describe("Original current user message. When supplied, extracted source fields must be literal excerpts of it."),
|
|
30
|
-
jd_text: sourceText.optional(),
|
|
31
|
-
student_profile: sourceText.optional(),
|
|
32
|
-
hr_reply: sourceText.optional(),
|
|
33
|
-
offers: z.array(z.object({ id: z.string().min(1).max(40), jd_text: sourceText.min(1), hr_reply: sourceText.optional() })).length(2).optional()
|
|
34
|
-
}).optional().describe("Separate original source documents, not model-computed facts or scores."),
|
|
35
|
-
previous_state: z.object({ schema_version: z.literal(1), records: z.array(sourceRecord).max(120) }).optional()
|
|
36
|
-
.describe("Previous next_state for this conversation and this offer only. Keep private; never share between users."),
|
|
37
|
-
mode: z.enum(["single_check", "negotiation", "interview_prep", "accept_conditions", "full_report"]).optional(),
|
|
38
|
-
new_assessment: z.boolean().optional().describe("True starts a different offer and discards previous_state.")
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
function jsonToolResult(result) {
|
|
42
|
-
return {
|
|
43
|
-
content: [
|
|
44
|
-
{
|
|
45
|
-
type: "text",
|
|
46
|
-
text: JSON.stringify(result, null, 2)
|
|
47
|
-
}
|
|
48
|
-
],
|
|
49
|
-
structuredContent: result
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function runTool(handler, input) {
|
|
54
|
-
try {
|
|
55
|
-
return jsonToolResult(handler(input));
|
|
56
|
-
} catch (error) {
|
|
57
|
-
return {
|
|
58
|
-
...jsonToolResult({ mcp_ok: false, error_code: "INVALID_ASSESSMENT_INPUT", error: error.message,
|
|
59
|
-
overall_score: null, report_text: "MCP 工具输入无效,暂不能生成 OfferGuard 风险报告。" }),
|
|
60
|
-
isError: true
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function createOfferGuardMcpServer() {
|
|
66
|
-
const server = new McpServer({
|
|
67
|
-
name: SERVICE_NAME,
|
|
68
|
-
version: SERVICE_VERSION
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
server.registerTool(
|
|
72
|
-
"offerguard_audit",
|
|
73
|
-
{
|
|
74
|
-
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.",
|
|
75
|
-
inputSchema: assessmentInput
|
|
76
|
-
},
|
|
77
|
-
async input => runTool(auditOfferGuardInput, input)
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
server.registerTool(
|
|
81
|
-
"risk_term_matcher",
|
|
82
|
-
{
|
|
83
|
-
description: "Match internship JD risk terms and return severity, source evidence, and trigger reasons.",
|
|
84
|
-
inputSchema: {
|
|
85
|
-
jdText: z.string().min(1).describe("Raw internship JD or offer text.")
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
async input => runTool(matchRiskTerms, input)
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
server.registerTool(
|
|
92
|
-
"internship_compliance_checker",
|
|
93
|
-
{
|
|
94
|
-
description: "Check internship compliance red lines such as unpaid trials, deposits, training loans, work hours, salary payment, agreements, and IP ownership.",
|
|
95
|
-
inputSchema: {
|
|
96
|
-
jdText: z.string().min(1).describe("Raw internship JD text."),
|
|
97
|
-
offerText: z.string().optional().describe("Optional offer or HR message text."),
|
|
98
|
-
extraText: z.string().optional().describe("Optional extra context from the student.")
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
async input => runTool(checkInternshipCompliance, input)
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
server.registerTool(
|
|
105
|
-
"offer_score_calculator",
|
|
106
|
-
{
|
|
107
|
-
description: "Calculate OfferGuard seven-dimension scores, overall risk level, and recommendation route.",
|
|
108
|
-
inputSchema: {
|
|
109
|
-
jdText: z.string().min(1).describe("Raw internship JD text."),
|
|
110
|
-
studentProfile: z.union([z.string(), z.record(z.string(), z.any())]).describe("Student profile, resume summary, preferences, and risk tolerance."),
|
|
111
|
-
jobProfile: z.union([z.string(), z.record(z.string(), z.any())]).optional().describe("Optional parsed job profile."),
|
|
112
|
-
riskTermResult: z.record(z.string(), z.any()).optional().describe("Optional output from risk_term_matcher."),
|
|
113
|
-
complianceResult: z.record(z.string(), z.any()).optional().describe("Optional output from internship_compliance_checker.")
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
async input => runTool(calculateOfferScore, input)
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
return server;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
14
|
export function createApp({ host = process.env.HOST ?? "127.0.0.1" } = {}) {
|
|
123
15
|
const app = createMcpExpressApp({ host });
|
|
124
16
|
const transports = {};
|
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();
|
package/docs/SCORING.md
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# 评分规则与边界
|
|
2
|
-
|
|
3
|
-
规则版本:2026-09-evidence-1。所有分数是确定性规则估计,未使用真实录用结果校准;不是百分比概率、法律判断或行业排名。
|
|
4
|
-
|
|
5
|
-
## 来源与状态
|
|
6
|
-
|
|
7
|
-
只有 `jd` 和 `hr` 来源参与岗位事实匹配。`student` 只参与个人背景和偏好检查,`unattributed` 不作为公司事实。
|
|
8
|
-
|
|
9
|
-
每个事实包含 `affirmed`、`negated`、`unknown` 或 `conflict`,并保留原文、来源类型和来源标识。来源表示由用户提供的材料类型,不表示平台已经核实 HR 身份或材料真伪。
|
|
10
|
-
|
|
11
|
-
词典以句子和逗号分句识别常见否定、疑问与不确定表达。双重否定等不明确表达保守置为未知。不同来源的明确肯定与否定,或不同薪资金额,形成冲突;后续说法不会自动覆盖旧说法。
|
|
12
|
-
|
|
13
|
-
无薪试岗、押金、培训贷仅作为重大风险信号。代码或作品归公司不自动判定违规;明确禁止展示时提示核实使用边界。
|
|
14
|
-
|
|
15
|
-
## 七维公式
|
|
16
|
-
|
|
17
|
-
以下公式仅在所需事实已知时执行。证据不足的维度为 null,不以默认分代替。
|
|
18
|
-
|
|
19
|
-
| 维度 | 已知时的计算 |
|
|
20
|
-
| --- | --- |
|
|
21
|
-
| 岗位匹配 | 40 + 45 × 学生匹配的岗位方向数 / 岗位方向数;岗位和学生文本独立比较 |
|
|
22
|
-
| 成长价值 | 导师明确存在40/明确不存在10 + 项目产出明确存在45/不存在10 |
|
|
23
|
-
| 薪资透明 | 金额已知且提及发放安排85;仅金额65;同时仍明确面议45 |
|
|
24
|
-
| 工作强度 | 工时已知:明确不加班85;未确定加班边界65;明确加班45;同时冲突于已识别学生时间偏好25 |
|
|
25
|
-
| 合规安全 | 明确重大风险0;明确不签协议20;协议明确75,不能理解为已经证明合法 |
|
|
26
|
-
| 转正清晰 | 标准明确存在80,明确不存在30;岗位不提供且学生不要求转正时不适用 |
|
|
27
|
-
| 简历增益 | 展示/证明材料明确存在40/不存在10 + 项目明确存在45/不存在10 |
|
|
28
|
-
|
|
29
|
-
总分仅在岗位职责、学生背景、薪资底线及到岗信息、岗位城市、金额、协议、工时有依据、无冲突且所有适用维度可评估时,取适用维度均分并四舍五入。总分为空时仍可报告有证据的风险。
|
|
30
|
-
|
|
31
|
-
明确重大风险优先进入 reject;矛盾证据进入 verify 并暂停总分;关键资料缺失进入 followup。资料齐全、无已命中风险、无低于50的适用维度、均分至少75且薪资未低于可比的个人底线,才进入 advance;否则 verify。
|
|
32
|
-
|
|
33
|
-
目前薪资底线只比较同单位日薪或月薪,不擅自换算每月工作天数。方向匹配不是技能掌握水平测量。城市名称、工时表达和否定结构的识别覆盖有限;未识别时需要补充明确格式。
|
|
34
|
-
|
|
35
|
-
## 已知限制
|
|
36
|
-
|
|
37
|
-
- 不抓取企业信用、招聘网站或市场薪酬;不得编造外部核验结果。
|
|
38
|
-
- 否定识别为有限规则,不是通用中文语义理解;反例测试只证明覆盖的样例。
|
|
39
|
-
- 学生只填写期望但未给出项目细节时,方向匹配仍可能粗糙;不能据此声称全面能力评估。
|
|
40
|
-
- HR 模糊承诺的真实性、法律适用关系、通勤成本及实际岗位成长仍需人工核实。
|
|
41
|
-
- 冲突解除暂采用完整核实材料开启新评估,未实现逐条签署确认。
|
|
42
|
-
- 线上知识库、会话变量和客户端错误分支未验证前,不宣称平台端闭环已完成。
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# 星辰工作流接入记录与操作规范
|
|
2
|
-
|
|
3
|
-
状态:待线上读取与实施。目标地址:
|
|
4
|
-
https://agent.xfyun.cn/agentbuilder/work_flow/621127/arrange?botId=5673753
|
|
5
|
-
|
|
6
|
-
浏览器工具可以列出标签页,但读取该标签页反复超时。没有导出当前配置,没有修改线上节点,没有发布或提交审核。历史本地配置不能替代当前线上备份。
|
|
7
|
-
|
|
8
|
-
## 先核对并备份
|
|
9
|
-
|
|
10
|
-
1. 读取当前工作流版本、节点、连线、输入引用及 MCP 服务启动配置。通过平台导出功能保存当前配置,并记录版本与导出时间。
|
|
11
|
-
2. 确认 MCP 是平台托管的包还是远程 URL,记录工具 `tools/list` 的真实 schema 与当前版本。不得因为本地存在0.4.0就认定线上已部署。
|
|
12
|
-
3. 验证会话变量的生命周期、用户隔离及新对话清空行为。若平台无法持久保存当前会话状态,则暂停该部分接入并报告,不能用全局变量或 Prompt 假装记忆。
|
|
13
|
-
|
|
14
|
-
## 目标数据流
|
|
15
|
-
|
|
16
|
-
开始/当前会话状态 → 来源分离与任务识别 → 门禁分支 → 真实 MCP → 结果有效性分支 → 更新会话状态 → 展示报告。
|
|
17
|
-
|
|
18
|
-
信息缺失时先问最多三个问题。若已有明确收费或无薪要求,即使资料不足也进入真实 MCP 审计,展示有证据的重大风险。知识库只在需解释规则或查证时检索,不能增加用户没提供的岗位事实。
|
|
19
|
-
|
|
20
|
-
话术、反问与接受条件请求有状态时,直接调用同一 MCP 的相应 mode,无需重复长篇画像、专家报告和最终重写。新岗位清空旧状态。双 Offer 分开审计并要求选定一个 Offer 后继续追问。
|
|
21
|
-
|
|
22
|
-
## 节点与变量绑定
|
|
23
|
-
|
|
24
|
-
| 输入/输出 | 绑定 |
|
|
25
|
-
| --- | --- |
|
|
26
|
-
| raw_input | 当前用户原话,或来源分离节点返回的 envelope_json 字符串 |
|
|
27
|
-
| structured_input | 仅本轮岗位原文、学生原文、HR原文;双Offer使用独立offers列表 |
|
|
28
|
-
| previous_state | 当前用户、当前会话、当前岗位保存的上轮 next_state |
|
|
29
|
-
| mode | single_check / negotiation / interview_prep / accept_conditions / full_report |
|
|
30
|
-
| new_assessment | 切换新岗位时 true |
|
|
31
|
-
| 下一轮状态 | MCP成功后保存 next_state;对比模式保存每个offers元素中的独立next_state |
|
|
32
|
-
| 可见输出 | 经有效性检查后的 report_text;不把整个result包装成代码块 |
|
|
33
|
-
|
|
34
|
-
如果平台只能绑定字符串,使用 README 中 `offerguard_schema:1` JSON envelope。应由结构化序列化节点组合状态;不要让模型重写 previous_state。源码包含真实 MCP 对该 envelope 的验证。
|
|
35
|
-
|
|
36
|
-
## 清理旧规则
|
|
37
|
-
|
|
38
|
-
- 删除把当前日期写死为六月的所有指令,统一使用 MCP report_date。
|
|
39
|
-
- 删除 demo_advance 指定82-90分、demo_compare必须推荐A等规则。
|
|
40
|
-
- 删除简短真实输入自动补造城市、薪资或背景的规则。演示按钮只发送完整、标注为合成的案例输入。
|
|
41
|
-
- 多专家分析如继续保留,只负责解释真实证据与未知项,不改工具分数、路线或来源。未经独立节点验证,不在材料中宣称真实专家并行。
|
|
42
|
-
- 比较 route 的精确字段值,不用包含通用“ROUTE_REAL”判断所有分支。
|
|
43
|
-
|
|
44
|
-
## 失败分支
|
|
45
|
-
|
|
46
|
-
MCP连接失败、超时、CallToolResult.isError、mcp_ok不为true、结果为空或缺少当前模式所需report_text时,输出:
|
|
47
|
-
|
|
48
|
-
MCP 工具调用失败,暂不能生成 OfferGuard 风险报告。请稍后重试。
|
|
49
|
-
|
|
50
|
-
不要保存失败状态,不输出上次报告作为本次成功结果,不调用模型重算风险作替代。用户-facing话术不暴露内部服务地址、密钥和堆栈。
|
|
51
|
-
|
|
52
|
-
## 线上验收清单
|
|
53
|
-
|
|
54
|
-
- 本地候选包的安装/服务启动与 tools/list 中的0.4.0版本一致。
|
|
55
|
-
- 单岗位、缺信息、重大风险、不同专业、不同薪资底线、双Offer、谈判模式各运行一次。
|
|
56
|
-
- 三轮闭环:缺薪资 → HR补充薪资 → HR改口产生矛盾;查看真实请求、状态保存、changes和报告。
|
|
57
|
-
- 新会话及新岗位不携带其他岗位/用户资料。
|
|
58
|
-
- 停止测试服务或使用隔离测试环境制造连接失败,验证用户侧失败分支。
|
|
59
|
-
- 每次记录请求时间、各节点耗时、最终耗时、成功/失败及脱敏截图。不得将本地毫秒数据当成线上端到端时间。
|
|
60
|
-
- 通过后冻结工作流、知识库、规则、包版本及其校验值,再录最终视频与生成复赛材料。
|
|
61
|
-
|
|
62
|
-
## 当前未完成
|
|
63
|
-
|
|
64
|
-
线上备份、节点修改、状态变量绑定、服务升级、真实平台调用及故障分支验证均未完成。不能将本文件当成已执行记录。
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
|
-
import { auditOfferGuardInput } from "../src/offerguard-tools.js";
|
|
7
|
-
import { TOOL_VERSION } from "../src/decision-engine.js";
|
|
8
|
-
import { CASES, GOOD_JD, PROFILE } from "../test/fixtures.js";
|
|
9
|
-
|
|
10
|
-
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
-
const directory = path.join(root, "outputs", "retest-v040");
|
|
12
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
13
|
-
const testedAt = new Date().toISOString();
|
|
14
|
-
const started = performance.now();
|
|
15
|
-
const tested = spawnSync(process.execPath, ["--test", "--test-reporter=tap", "test/decision-engine.test.js", "test/mcp-protocol.test.js"], { cwd: root, encoding: "utf8", timeout: 60000, maxBuffer: 4 * 1024 * 1024 });
|
|
16
|
-
const log = `${tested.stdout ?? ""}\n${tested.stderr ?? ""}`;
|
|
17
|
-
fs.writeFileSync(path.join(directory, "tests.tap"), log);
|
|
18
|
-
const count = label => Number(log.match(new RegExp(`^# ${label} (\\d+)$`, "m"))?.[1] ?? 0);
|
|
19
|
-
const success = tested.status === 0 && count("tests") > 0 && count("fail") === 0;
|
|
20
|
-
const inputs = { ...CASES, compare: { structured_input: { student_profile: PROFILE, offers: [{ id: "A", jd_text: GOOD_JD }, { id: "B", jd_text: "北京后端实习生,须缴纳押金800元。" }] } } };
|
|
21
|
-
const samples = {};
|
|
22
|
-
const runs = [];
|
|
23
|
-
for (const [name, input] of Object.entries(inputs)) {
|
|
24
|
-
const r = auditOfferGuardInput(input);
|
|
25
|
-
samples[name] = { input, result: r };
|
|
26
|
-
// This measures local rule execution only, excluding MCP transport, models and the platform.
|
|
27
|
-
const times = Array.from({ length: 20 }, () => {
|
|
28
|
-
const t = performance.now(); auditOfferGuardInput(input); return performance.now() - t;
|
|
29
|
-
}).sort((a, b) => a - b);
|
|
30
|
-
runs.push({ name, samples: times.length, local_median_ms: +times[Math.floor(times.length / 2)].toFixed(3), local_p95_ms: +times[Math.ceil(times.length * 0.95) - 1].toFixed(3), route: r.route, score: r.overall_score });
|
|
31
|
-
}
|
|
32
|
-
const first = auditOfferGuardInput({ structured_input: { jd_text: GOOD_JD.replace("薪资180元/天,每月15日发放。", ""), student_profile: PROFILE } });
|
|
33
|
-
const secondInput = { raw_input: "HR回复:薪资180元/天,每月15日发放。", previous_state: first.next_state };
|
|
34
|
-
const second = auditOfferGuardInput(secondInput);
|
|
35
|
-
const thirdInput = { raw_input: "HR回复:薪资100元/天。", previous_state: second.next_state };
|
|
36
|
-
const third = auditOfferGuardInput(thirdInput);
|
|
37
|
-
samples.evidence_update = { first, second: { input: secondInput, result: second }, third: { input: thirdInput, result: third } };
|
|
38
|
-
const sourceFiles = ["package.json", "package-lock.json", "src/decision-engine.js", "src/offerguard-tools.js", "src/server.js", "src/stdio.js", "test/decision-engine.test.js", "test/mcp-protocol.test.js", "test/fixtures.js"];
|
|
39
|
-
const checksums = Object.fromEntries(sourceFiles.map(p => [p, fs.existsSync(path.join(root, p)) ? createHash("sha256").update(fs.readFileSync(path.join(root, p))).digest("hex") : null]));
|
|
40
|
-
const report = { release_status: "local_candidate_not_online_verified", version: TOOL_VERSION, tested_at_utc: testedAt, node: process.version,
|
|
41
|
-
test_result: { passed: success, total: count("tests"), pass: count("pass"), fail: count("fail"), exit_code: tested.status, error: tested.error?.message ?? null, elapsed_ms: Math.round(performance.now() - started) },
|
|
42
|
-
local_rule_timings: runs, reproducibility_lock_present: fs.existsSync(path.join(root, "package-lock.json")), online: { verified: false, timings: null, blocker: "Browser tab enumeration works but reading the XFYun workflow tab repeatedly timed out; no current online backup, mutation or validation performed." }, source_sha256: checksums };
|
|
43
|
-
fs.writeFileSync(path.join(directory, "validation.json"), JSON.stringify(report, null, 2));
|
|
44
|
-
fs.writeFileSync(path.join(directory, "demo-results.json"), JSON.stringify(samples, null, 2));
|
|
45
|
-
const markdown = ["# OfferGuard 0.4.0 本地候选版验证", `测试时间(UTC):${testedAt}`, `结果:${success ? "通过" : "失败"};${count("pass")}/${count("tests")};Node ${process.version}。`,
|
|
46
|
-
"真实协议覆盖:HTTP、SSE、stdio 的初始化、工具发现、四工具调用、状态更新、双 Offer 及输入错误;另有 HTTP 服务断开测试。", "线上状态:未验收。未读取当前配置,未修改或发布线上工作流。", "## 合成场景实测",
|
|
47
|
-
["| 场景 | 路线 | 综合分 | 本地规则中位耗时(ms) |", "| --- | --- | --- | --- |", ...runs.map(r => `| ${r.name} | ${r.route} | ${r.score ?? "未评分"} | ${r.local_median_ms} |`)].join("\n"),
|
|
48
|
-
"这些耗时仅是本地规则函数执行,排除网络、MCP传输、模型与星辰平台耗时,不可作为线上响应时间。",
|
|
49
|
-
`三轮证据更新:${first.route}/${first.overall_score ?? "未评分"} → ${second.route}/${second.overall_score ?? "未评分"} → ${third.route}/${third.overall_score ?? "未评分"}。`,
|
|
50
|
-
"测试日志见 tests.tap,完整合成输入/输出见 demo-results.json,源码校验值见 validation.json。未使用真实学生数据,未测量现实场景准确率。", "当前候选版不得作为最终冻结版或已上线证明。"];
|
|
51
|
-
fs.writeFileSync(path.join(directory, "validation.md"), markdown.join("\n\n"));
|
|
52
|
-
console.log(JSON.stringify({ passed: success, tests: report.test_result, output: directory, online_verified: false }, null, 2));
|
|
53
|
-
if (!success) process.exitCode = 1;
|