fmes-contact-mcp 0.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/.env +3 -0
- package/.env.example +6 -0
- package/README.md +90 -0
- package/dist/client/crm-client.d.ts +13 -0
- package/dist/client/crm-client.js +126 -0
- package/dist/client/crm-client.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/get-promoter-contracts.d.ts +30 -0
- package/dist/tools/get-promoter-contracts.js +54 -0
- package/dist/tools/get-promoter-contracts.js.map +1 -0
- package/dist/tools/get-promoter-detail.d.ts +22 -0
- package/dist/tools/get-promoter-detail.js +40 -0
- package/dist/tools/get-promoter-detail.js.map +1 -0
- package/dist/tools/search-contracts.d.ts +31 -0
- package/dist/tools/search-contracts.js +50 -0
- package/dist/tools/search-contracts.js.map +1 -0
- package/dist/tools/search-promoters.d.ts +61 -0
- package/dist/tools/search-promoters.js +58 -0
- package/dist/tools/search-promoters.js.map +1 -0
- package/dist/tools/stats-promoters.d.ts +16 -0
- package/dist/tools/stats-promoters.js +19 -0
- package/dist/tools/stats-promoters.js.map +1 -0
- package/dist/types/index.d.ts +87 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +25 -0
- package/src/client/crm-client.ts +135 -0
- package/src/index.ts +88 -0
- package/src/tools/get-promoter-contracts.ts +69 -0
- package/src/tools/get-promoter-detail.ts +48 -0
- package/src/tools/search-contracts.ts +62 -0
- package/src/tools/search-promoters.ts +68 -0
- package/src/tools/stats-promoters.ts +25 -0
- package/src/types/index.ts +98 -0
- package/test/test-client.ts +68 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { crmClient } from "../client/crm-client.js";
|
|
3
|
+
export const searchPromotersSchema = z.object({
|
|
4
|
+
name: z.string().optional().describe("触点姓名"),
|
|
5
|
+
idcard: z.string().optional().describe("身份证号"),
|
|
6
|
+
tel: z.string().optional().describe("手机号"),
|
|
7
|
+
empPk: z.string().optional().describe("促销员ID(米咖ID或普洛特ID)"),
|
|
8
|
+
province: z.string().optional().describe("省份名称,如:北京、上海"),
|
|
9
|
+
city: z.string().optional().describe("城市名称,如:北京、上海、广州"),
|
|
10
|
+
area: z.string().optional().describe("区县名称"),
|
|
11
|
+
sex: z.number().int().min(0).max(1).optional().describe("性别(0=女, 1=男)"),
|
|
12
|
+
ageMin: z.number().int().min(18).max(70).optional().describe("最小年龄"),
|
|
13
|
+
ageMax: z.number().int().min(18).max(70).optional().describe("最大年龄"),
|
|
14
|
+
workMonthMin: z.number().int().min(0).optional().describe("最少服务月数"),
|
|
15
|
+
projectNumMin: z.number().int().min(0).optional().describe("最少项目数"),
|
|
16
|
+
status: z
|
|
17
|
+
.enum(["项目中", "在岗", "待岗中"])
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("项目状态"),
|
|
20
|
+
limit: z.number().int().min(1).max(50).default(10).describe("返回数量上限"),
|
|
21
|
+
});
|
|
22
|
+
export async function searchPromoters(args, _extra) {
|
|
23
|
+
const result = await crmClient.searchPromoters({
|
|
24
|
+
name: args.name,
|
|
25
|
+
idcard: args.idcard,
|
|
26
|
+
tel: args.tel,
|
|
27
|
+
empPk: args.empPk,
|
|
28
|
+
province: args.province,
|
|
29
|
+
city: args.city,
|
|
30
|
+
area: args.area,
|
|
31
|
+
sex: args.sex,
|
|
32
|
+
ageMin: args.ageMin,
|
|
33
|
+
ageMax: args.ageMax,
|
|
34
|
+
workMonthMin: args.workMonthMin,
|
|
35
|
+
projectNumMin: args.projectNumMin,
|
|
36
|
+
status: args.status,
|
|
37
|
+
limit: args.limit,
|
|
38
|
+
});
|
|
39
|
+
if (result.list.length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: "未找到符合条件的触点。" }],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const summary = `找到 ${result.total} 个触点,以下是前 ${result.list.length} 个:\n\n` +
|
|
45
|
+
result.list
|
|
46
|
+
.map((p, i) => `${i + 1}. ${p.name} | 米咖ID: ${p.empPk} | 性别: ${p.sex === 1 ? "男" : "女"} | 年龄: ${p.age} | ` +
|
|
47
|
+
`地区: ${p.province || ""}${p.city || ""}${p.area || ""} | 状态: ${p.status || "未知"} | ` +
|
|
48
|
+
`服务月数: ${p.workMonth || 0} | 项目数: ${p.projectNum || 0}`)
|
|
49
|
+
.join("\n") +
|
|
50
|
+
(result.total > result.list.length
|
|
51
|
+
? `\n\n(共 ${result.total} 条,已截取前 ${result.list.length} 条,可缩小筛选条件或调整 limit)`
|
|
52
|
+
: "");
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text", text: summary }],
|
|
55
|
+
structuredContent: result,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=search-promoters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-promoters.js","sourceRoot":"","sources":["../../src/tools/search-promoters.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const statsPromotersSchema: z.ZodObject<{
|
|
3
|
+
city: z.ZodOptional<z.ZodString>;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
city?: string | undefined;
|
|
6
|
+
}, {
|
|
7
|
+
city?: string | undefined;
|
|
8
|
+
}>;
|
|
9
|
+
export type StatsPromotersInput = z.infer<typeof statsPromotersSchema>;
|
|
10
|
+
export declare function statsPromoters(_args: StatsPromotersInput, _extra?: unknown): Promise<{
|
|
11
|
+
content: {
|
|
12
|
+
type: "text";
|
|
13
|
+
text: string;
|
|
14
|
+
}[];
|
|
15
|
+
structuredContent: import("../types/index.js").StatsResult;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { crmClient } from "../client/crm-client.js";
|
|
3
|
+
export const statsPromotersSchema = z.object({
|
|
4
|
+
city: z.string().optional().describe("城市名称"),
|
|
5
|
+
});
|
|
6
|
+
export async function statsPromoters(_args, _extra) {
|
|
7
|
+
const result = await crmClient.statsPromoters();
|
|
8
|
+
const text = `触点统计概览(共 ${result.total} 人)\n\n` +
|
|
9
|
+
`按项目状态分布:\n` +
|
|
10
|
+
result.groups
|
|
11
|
+
.map((g) => ` ${g.key}: ${g.count}人(${g.percentage})`)
|
|
12
|
+
.join("\n") +
|
|
13
|
+
`\n\n提示:可按地区进一步筛选,使用 search_promoters 工具指定 city 参数。`;
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text }],
|
|
16
|
+
structuredContent: result,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=stats-promoters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats-promoters.js","sourceRoot":"","sources":["../../src/tools/stats-promoters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC;CAC7C,CAAC,CAAC;AAIH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,KAA0B,EAAE,MAAgB;IAC/E,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,cAAc,EAAE,CAAC;IAEhD,MAAM,IAAI,GACR,YAAY,MAAM,CAAC,KAAK,SAAS;QACjC,YAAY;QACZ,MAAM,CAAC,MAAM;aACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,UAAU,GAAG,CAAC;aACtD,IAAI,CAAC,IAAI,CAAC;QACb,oDAAoD,CAAC;IAEvD,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,CAAC;QAC1C,iBAAiB,EAAE,MAAM;KAC1B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export interface SearchPromotersParams {
|
|
2
|
+
name?: string;
|
|
3
|
+
idcard?: string;
|
|
4
|
+
tel?: string;
|
|
5
|
+
empPk?: string;
|
|
6
|
+
province?: string;
|
|
7
|
+
city?: string;
|
|
8
|
+
area?: string;
|
|
9
|
+
sex?: number;
|
|
10
|
+
ageMin?: number;
|
|
11
|
+
ageMax?: number;
|
|
12
|
+
workMonthMin?: number;
|
|
13
|
+
projectNumMin?: number;
|
|
14
|
+
status?: string;
|
|
15
|
+
limit: number;
|
|
16
|
+
}
|
|
17
|
+
export interface SearchContractsParams {
|
|
18
|
+
idcard?: string;
|
|
19
|
+
empPk?: string;
|
|
20
|
+
contractType?: "1" | "2";
|
|
21
|
+
limit: number;
|
|
22
|
+
}
|
|
23
|
+
export interface StatsPromotersParams {
|
|
24
|
+
province?: string;
|
|
25
|
+
city?: string;
|
|
26
|
+
groupBy: "age" | "region" | "status";
|
|
27
|
+
}
|
|
28
|
+
export interface PromoterInfo {
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
name: string;
|
|
31
|
+
sex: number;
|
|
32
|
+
age: number;
|
|
33
|
+
tel: string;
|
|
34
|
+
idcard: string;
|
|
35
|
+
empPk: string;
|
|
36
|
+
province: string;
|
|
37
|
+
city: string;
|
|
38
|
+
area: string;
|
|
39
|
+
homeAddress: string;
|
|
40
|
+
bank: string;
|
|
41
|
+
bankNum: string;
|
|
42
|
+
projectNum: number;
|
|
43
|
+
workMonth: number;
|
|
44
|
+
status: string;
|
|
45
|
+
registerTime: string;
|
|
46
|
+
isAuthentication: number;
|
|
47
|
+
}
|
|
48
|
+
export interface McpContractInfo {
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
contractType: string;
|
|
51
|
+
source: string;
|
|
52
|
+
customer: string;
|
|
53
|
+
serviceSupplier: string;
|
|
54
|
+
merchantName: string;
|
|
55
|
+
signedTime: string;
|
|
56
|
+
startTime: string;
|
|
57
|
+
endTime: string;
|
|
58
|
+
taskName: string;
|
|
59
|
+
clientId: string;
|
|
60
|
+
contractTypeId: string;
|
|
61
|
+
startDate: string;
|
|
62
|
+
endDate: string;
|
|
63
|
+
contractNumber: string;
|
|
64
|
+
contractStatus: string;
|
|
65
|
+
}
|
|
66
|
+
export interface SearchPromotersResult {
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
total: number;
|
|
69
|
+
list: PromoterInfo[];
|
|
70
|
+
query: Partial<SearchPromotersParams>;
|
|
71
|
+
}
|
|
72
|
+
export interface SearchContractsResult {
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
total: number;
|
|
75
|
+
list: McpContractInfo[];
|
|
76
|
+
query: Partial<SearchContractsParams>;
|
|
77
|
+
}
|
|
78
|
+
export interface StatsResult {
|
|
79
|
+
[key: string]: unknown;
|
|
80
|
+
query: Partial<StatsPromotersParams>;
|
|
81
|
+
total: number;
|
|
82
|
+
groups: Array<{
|
|
83
|
+
key: string;
|
|
84
|
+
count: number;
|
|
85
|
+
percentage: string;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,+BAA+B"}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fmes-contact-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "触点推荐 MCP Server - 为企业 AI Agent 提供促销员智能推荐",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch",
|
|
10
|
+
"start": "node dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
14
|
+
"axios": "^1.7.0",
|
|
15
|
+
"axios-cookiejar-support": "^7.0.0",
|
|
16
|
+
"dotenv": "^17.4.2",
|
|
17
|
+
"tough-cookie": "^6.0.1",
|
|
18
|
+
"zod": "^3.23.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"tsx": "^4.22.4",
|
|
23
|
+
"typescript": "^5.5.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from "axios";
|
|
2
|
+
import { wrapper } from "axios-cookiejar-support";
|
|
3
|
+
import { CookieJar } from "tough-cookie";
|
|
4
|
+
import {
|
|
5
|
+
SearchPromotersParams,
|
|
6
|
+
SearchPromotersResult,
|
|
7
|
+
SearchContractsParams,
|
|
8
|
+
SearchContractsResult,
|
|
9
|
+
StatsResult,
|
|
10
|
+
PromoterInfo,
|
|
11
|
+
McpContractInfo,
|
|
12
|
+
} from "../types/index.js";
|
|
13
|
+
|
|
14
|
+
const CRM_BASE_URL = process.env.CRM_BASE_URL || "http://localhost:8080/rest";
|
|
15
|
+
const CRM_USERNAME = process.env.CRM_USERNAME || "";
|
|
16
|
+
const CRM_PASSWORD = process.env.CRM_PASSWORD || "";
|
|
17
|
+
|
|
18
|
+
class CrmClient {
|
|
19
|
+
private client: AxiosInstance;
|
|
20
|
+
private loggedIn: boolean = false;
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
const jar = new CookieJar();
|
|
24
|
+
this.client = wrapper(
|
|
25
|
+
axios.create({
|
|
26
|
+
baseURL: CRM_BASE_URL,
|
|
27
|
+
timeout: 30000,
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
jar,
|
|
30
|
+
withCredentials: true,
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async login(): Promise<void> {
|
|
36
|
+
const res = await this.client.get("/sysUser/sysUserLogin", {
|
|
37
|
+
params: { account: CRM_USERNAME, password: CRM_PASSWORD },
|
|
38
|
+
});
|
|
39
|
+
if (res.status === 200) {
|
|
40
|
+
this.loggedIn = true;
|
|
41
|
+
} else {
|
|
42
|
+
throw new Error(`登录失败: ${res.status} ${res.statusText}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async searchPromoters(
|
|
47
|
+
params: SearchPromotersParams
|
|
48
|
+
): Promise<SearchPromotersResult> {
|
|
49
|
+
if (!this.loggedIn) await this.login();
|
|
50
|
+
|
|
51
|
+
const body: Record<string, unknown> = {
|
|
52
|
+
limit: params.limit,
|
|
53
|
+
};
|
|
54
|
+
if (params.name) body.name = params.name;
|
|
55
|
+
if (params.idcard) body.idcard = params.idcard;
|
|
56
|
+
if (params.tel) body.tel = params.tel;
|
|
57
|
+
if (params.empPk) body.empPk = params.empPk;
|
|
58
|
+
if (params.province) body.province = params.province;
|
|
59
|
+
if (params.city) body.city = params.city;
|
|
60
|
+
if (params.area) body.area = params.area;
|
|
61
|
+
if (params.sex !== undefined) body.sex = params.sex;
|
|
62
|
+
if (params.ageMin !== undefined) body.ageMin = params.ageMin;
|
|
63
|
+
if (params.ageMax !== undefined) body.ageMax = params.ageMax;
|
|
64
|
+
if (params.workMonthMin !== undefined) body.workMonthMin = params.workMonthMin;
|
|
65
|
+
if (params.projectNumMin !== undefined) body.projectNumMin = params.projectNumMin;
|
|
66
|
+
if (params.status) body.status = params.status;
|
|
67
|
+
|
|
68
|
+
const res = await this.client.post("/mcp/salesman/search", body);
|
|
69
|
+
const data = res.data as { data: PromoterInfo[] };
|
|
70
|
+
const list = data.data || [];
|
|
71
|
+
return {
|
|
72
|
+
total: list.length,
|
|
73
|
+
list,
|
|
74
|
+
query: params,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getPromoterDetail(empPk?: string, idcard?: string): Promise<PromoterInfo | null> {
|
|
79
|
+
if (!this.loggedIn) await this.login();
|
|
80
|
+
|
|
81
|
+
const body: Record<string, unknown> = {};
|
|
82
|
+
if (empPk) body.empPk = empPk;
|
|
83
|
+
if (idcard) body.idcard = idcard;
|
|
84
|
+
|
|
85
|
+
const res = await this.client.post("/mcp/salesman/detail", body);
|
|
86
|
+
const data = res.data as { data: PromoterInfo };
|
|
87
|
+
return data.data || null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async getContracts(
|
|
91
|
+
params: SearchContractsParams
|
|
92
|
+
): Promise<SearchContractsResult> {
|
|
93
|
+
if (!this.loggedIn) await this.login();
|
|
94
|
+
|
|
95
|
+
const body: Record<string, unknown> = {};
|
|
96
|
+
if (params.idcard) body.idcard = params.idcard;
|
|
97
|
+
if (params.empPk) body.empPk = params.empPk;
|
|
98
|
+
if (params.contractType) body.contractType = params.contractType;
|
|
99
|
+
|
|
100
|
+
const res = await this.client.post("/mcp/salesman/contract/search", body);
|
|
101
|
+
const data = res.data as { data: { list: McpContractInfo[]; total: number } };
|
|
102
|
+
const result = data.data || { list: [], total: 0 };
|
|
103
|
+
return {
|
|
104
|
+
total: result.total,
|
|
105
|
+
list: result.list,
|
|
106
|
+
query: params,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async statsPromoters(): Promise<StatsResult> {
|
|
111
|
+
if (!this.loggedIn) await this.login();
|
|
112
|
+
|
|
113
|
+
const res = await this.client.post("/mcp/salesman/search", { limit: 500 });
|
|
114
|
+
const data = res.data as { data: PromoterInfo[] };
|
|
115
|
+
const list = data.data || [];
|
|
116
|
+
|
|
117
|
+
const groups: StatsResult["groups"] = [];
|
|
118
|
+
const groupMap = new Map<string, number>();
|
|
119
|
+
for (const p of list) {
|
|
120
|
+
const key = p.status || "未知";
|
|
121
|
+
groupMap.set(key, (groupMap.get(key) || 0) + 1);
|
|
122
|
+
}
|
|
123
|
+
for (const [key, count] of groupMap) {
|
|
124
|
+
groups.push({
|
|
125
|
+
key,
|
|
126
|
+
count,
|
|
127
|
+
percentage: list.length > 0 ? ((count / list.length) * 100).toFixed(1) + "%" : "0%",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { total: list.length, groups, query: { groupBy: "status" } };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const crmClient = new CrmClient();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
searchPromoters,
|
|
6
|
+
searchPromotersSchema,
|
|
7
|
+
} from "./tools/search-promoters.js";
|
|
8
|
+
import {
|
|
9
|
+
getPromoterDetail,
|
|
10
|
+
getPromoterDetailSchema,
|
|
11
|
+
} from "./tools/get-promoter-detail.js";
|
|
12
|
+
import {
|
|
13
|
+
getPromoterContracts,
|
|
14
|
+
getPromoterContractsSchema,
|
|
15
|
+
} from "./tools/get-promoter-contracts.js";
|
|
16
|
+
import {
|
|
17
|
+
searchContracts,
|
|
18
|
+
searchContractsSchema,
|
|
19
|
+
} from "./tools/search-contracts.js";
|
|
20
|
+
import {
|
|
21
|
+
statsPromoters,
|
|
22
|
+
statsPromotersSchema,
|
|
23
|
+
} from "./tools/stats-promoters.js";
|
|
24
|
+
|
|
25
|
+
const server = new McpServer({
|
|
26
|
+
name: "fmes-contact-mcp",
|
|
27
|
+
version: "0.1.0",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
server.registerTool(
|
|
31
|
+
"search_promoters",
|
|
32
|
+
{
|
|
33
|
+
description:
|
|
34
|
+
"按多维度筛选触点(促销员),支持按地区、年龄、服务月数、项目数、星级、状态等条件筛选。",
|
|
35
|
+
inputSchema: searchPromotersSchema,
|
|
36
|
+
},
|
|
37
|
+
searchPromoters
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
server.registerTool(
|
|
41
|
+
"get_promoter_detail",
|
|
42
|
+
{
|
|
43
|
+
description:
|
|
44
|
+
"获取指定触点的详细信息,包括姓名、身份证号、手机号、银行卡、星级、信用分、项目数、服务月数等。",
|
|
45
|
+
inputSchema: getPromoterDetailSchema,
|
|
46
|
+
},
|
|
47
|
+
getPromoterDetail
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
server.registerTool(
|
|
51
|
+
"get_promoter_contracts",
|
|
52
|
+
{
|
|
53
|
+
description:
|
|
54
|
+
"查询某个触点的所有合同信息,包括长促合同和灵工合同,返回客户名称、签约平台、合同期限等。",
|
|
55
|
+
inputSchema: getPromoterContractsSchema,
|
|
56
|
+
},
|
|
57
|
+
getPromoterContracts
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
server.registerTool(
|
|
61
|
+
"search_contracts",
|
|
62
|
+
{
|
|
63
|
+
description:
|
|
64
|
+
"按合同维度搜索,支持按身份证号、客户名称、签约平台、合同类型、时间范围筛选。",
|
|
65
|
+
inputSchema: searchContractsSchema,
|
|
66
|
+
},
|
|
67
|
+
searchContracts
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
server.registerTool(
|
|
71
|
+
"stats_promoters",
|
|
72
|
+
{
|
|
73
|
+
description: "触点统计概览,查看触点的分布情况(按状态、地区等维度)。",
|
|
74
|
+
inputSchema: statsPromotersSchema,
|
|
75
|
+
},
|
|
76
|
+
statsPromoters
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
async function main() {
|
|
80
|
+
const transport = new StdioServerTransport();
|
|
81
|
+
await server.connect(transport);
|
|
82
|
+
console.error("fmes-contact-mcp Server 已启动,等待连接...");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
main().catch((err) => {
|
|
86
|
+
console.error("启动失败:", err);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { crmClient } from "../client/crm-client.js";
|
|
3
|
+
import type { McpContractInfo } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
export const getPromoterContractsSchema = z.object({
|
|
6
|
+
empPk: z.string().describe("触点促销员ID(米咖ID)"),
|
|
7
|
+
contractType: z
|
|
8
|
+
.enum(["1", "2"])
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("合同类型(1=灵工合同, 2=长促合同),不传则查全部"),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type GetPromoterContractsInput = z.infer<
|
|
14
|
+
typeof getPromoterContractsSchema
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
export async function getPromoterContracts(args: GetPromoterContractsInput, _extra?: unknown) {
|
|
18
|
+
const promoter = await crmClient.getPromoterDetail(args.empPk);
|
|
19
|
+
|
|
20
|
+
if (!promoter) {
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: "text" as const,
|
|
25
|
+
text: `未找到促销员ID为 ${args.empPk} 的触点。`,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = await crmClient.getContracts({
|
|
32
|
+
empPk: args.empPk,
|
|
33
|
+
contractType: args.contractType,
|
|
34
|
+
limit: 100,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const linggongList = result.list.filter((c) => c.contractType === "1");
|
|
38
|
+
const changcuList = result.list.filter((c) => c.contractType === "2");
|
|
39
|
+
|
|
40
|
+
const formatContract = (c: McpContractInfo, i: number) => {
|
|
41
|
+
const typeLabel = c.contractType === "1" ? "灵工" : "长促";
|
|
42
|
+
const sourceLabel = c.source ? `(${c.source})` : "";
|
|
43
|
+
return (
|
|
44
|
+
`${i + 1}. [${typeLabel}${sourceLabel}] 客户: ${c.customer || "未知"} | ` +
|
|
45
|
+
`签约时间: ${c.signedTime || "未知"} | 有效期: ${c.startTime || "?"} ~ ${c.endTime || "?"} | ` +
|
|
46
|
+
`状态: ${c.contractStatus || "未知"}`
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const text =
|
|
51
|
+
`触点:${promoter.name}(${args.empPk})的合同信息\n\n` +
|
|
52
|
+
`=== 灵工合同(${linggongList.length} 份)===\n` +
|
|
53
|
+
(linggongList.length === 0
|
|
54
|
+
? "无灵工合同记录\n"
|
|
55
|
+
: linggongList.map((c, i) => formatContract(c, i)).join("\n") + "\n") +
|
|
56
|
+
`\n=== 长促合同(${changcuList.length} 份)===\n` +
|
|
57
|
+
(changcuList.length === 0
|
|
58
|
+
? "无长促合同记录"
|
|
59
|
+
: changcuList.map((c, i) => formatContract(c, i)).join("\n"));
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text" as const, text }],
|
|
63
|
+
structuredContent: {
|
|
64
|
+
promoter,
|
|
65
|
+
linggongContracts: linggongList,
|
|
66
|
+
changcuContracts: changcuList,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { crmClient } from "../client/crm-client.js";
|
|
3
|
+
|
|
4
|
+
export const getPromoterDetailSchema = z.object({
|
|
5
|
+
empPk: z.string().describe("触点促销员ID(米咖ID)"),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export type GetPromoterDetailInput = z.infer<typeof getPromoterDetailSchema>;
|
|
9
|
+
|
|
10
|
+
export async function getPromoterDetail(args: GetPromoterDetailInput, _extra?: unknown) {
|
|
11
|
+
const p = await crmClient.getPromoterDetail(args.empPk);
|
|
12
|
+
|
|
13
|
+
if (!p) {
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: "text" as const,
|
|
18
|
+
text: `未找到促销员ID为 ${args.empPk} 的触点。`,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sexLabel = p.sex === 1 ? "男" : p.sex === 0 ? "女" : "未知";
|
|
25
|
+
const authLabel = p.isAuthentication === 1 ? "已认证" : "未认证";
|
|
26
|
+
|
|
27
|
+
const detail =
|
|
28
|
+
`姓名:${p.name}\n` +
|
|
29
|
+
`米咖ID:${p.empPk}\n` +
|
|
30
|
+
`性别:${sexLabel}\n` +
|
|
31
|
+
`年龄:${p.age}岁\n` +
|
|
32
|
+
`身份证号:${p.idcard}\n` +
|
|
33
|
+
`手机号:${p.tel}\n` +
|
|
34
|
+
`地区:${p.province || ""}${p.city || ""}${p.area || ""}\n` +
|
|
35
|
+
`地址:${p.homeAddress || "未知"}\n` +
|
|
36
|
+
`状态:${p.status || "未知"}\n` +
|
|
37
|
+
`项目数:${p.projectNum || 0}个\n` +
|
|
38
|
+
`服务月数:${p.workMonth || 0}个月\n` +
|
|
39
|
+
`开户行:${p.bank || "未知"}\n` +
|
|
40
|
+
`银行卡号:${p.bankNum || "未知"}\n` +
|
|
41
|
+
`注册时间:${p.registerTime || "未知"}\n` +
|
|
42
|
+
`实名认证:${authLabel}`;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: "text" as const, text: detail }],
|
|
46
|
+
structuredContent: p,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { crmClient } from "../client/crm-client.js";
|
|
3
|
+
import type { McpContractInfo } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
export const searchContractsSchema = z.object({
|
|
6
|
+
idcard: z.string().optional().describe("身份证号"),
|
|
7
|
+
empPk: z.string().optional().describe("促销员ID(米咖ID)"),
|
|
8
|
+
contractType: z
|
|
9
|
+
.enum(["1", "2"])
|
|
10
|
+
.optional()
|
|
11
|
+
.describe("合同类型(1=灵工合同, 2=长促合同),不传则查全部"),
|
|
12
|
+
limit: z.number().int().min(1).max(50).default(10).describe("返回数量上限"),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type SearchContractsInput = z.infer<typeof searchContractsSchema>;
|
|
16
|
+
|
|
17
|
+
export async function searchContracts(args: SearchContractsInput, _extra?: unknown) {
|
|
18
|
+
if (!args.idcard && !args.empPk) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: "text" as const,
|
|
23
|
+
text: "请至少提供身份证号或促销员ID中的一个条件。",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = await crmClient.getContracts({
|
|
30
|
+
idcard: args.idcard,
|
|
31
|
+
empPk: args.empPk,
|
|
32
|
+
contractType: args.contractType,
|
|
33
|
+
limit: args.limit,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (result.list.length === 0) {
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{ type: "text" as const, text: "未找到符合条件的合同记录。" },
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const formatContract = (c: McpContractInfo, i: number) => {
|
|
45
|
+
const typeLabel = c.contractType === "1" ? "灵工" : "长促";
|
|
46
|
+
const sourceLabel = c.source ? `(${c.source})` : "";
|
|
47
|
+
return (
|
|
48
|
+
`${i + 1}. [${typeLabel}${sourceLabel}] 客户: ${c.customer || "未知"} | ` +
|
|
49
|
+
`签约时间: ${c.signedTime || "未知"} | 有效期: ${c.startTime || "?"} ~ ${c.endTime || "?"} | ` +
|
|
50
|
+
`状态: ${c.contractStatus || "未知"}`
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const text =
|
|
55
|
+
`找到 ${result.total} 条合同记录:\n\n` +
|
|
56
|
+
result.list.map((c, i) => formatContract(c, i)).join("\n");
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text" as const, text }],
|
|
60
|
+
structuredContent: result,
|
|
61
|
+
};
|
|
62
|
+
}
|