@wukongcrm/mcp-server 0.2.4 → 0.2.5

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 CHANGED
@@ -397,13 +397,34 @@ wukong-mcp
397
397
  {
398
398
  "fieldName": "ownerUserId",
399
399
  "formType": "user",
400
- "type": 1,
400
+ "type": 3,
401
401
  "values": ["张三"]
402
402
  }
403
403
  ]
404
404
  }
405
405
  ```
406
406
 
407
+ 旧 CRM 的人员筛选只会处理操作 `3`(包含)、`4`(不包含)、`5`(为空)和 `6`(不为空);其中按人员 ID 匹配必须使用 `type=3`。MCP 的人员便捷参数会固定生成操作 `3`,显式传入未实现的人员操作会在请求后端前报错,避免返回未按人员过滤的数据。
408
+
409
+ 日期字段必须明确传 `formType=date` 或 `formType=datetime`。固定范围使用操作 `14`,`values` 按开始、结束顺序提供两个值;`date` 使用 `YYYY-MM-DD`,`datetime` 使用 `YYYY-MM-DD HH:mm:ss`。查询完整自然日时,边界为当天 `00:00:00` 和 `23:59:59`,后端范围两端均包含:
410
+
411
+ ```json
412
+ {
413
+ "module": "activity",
414
+ "createUserName": "<人员唯一昵称>",
415
+ "filters": [
416
+ {
417
+ "fieldName": "createTime",
418
+ "formType": "datetime",
419
+ "type": 14,
420
+ "values": ["YYYY-MM-DD 00:00:00", "YYYY-MM-DD 23:59:59"]
421
+ }
422
+ ]
423
+ }
424
+ ```
425
+
426
+ `filters` 和 `searchList` 是同一个高级筛选参数的两个名称,只能选一个。每项必须提供字段名和 `type`/`searchEnum`;两种操作码同时出现时必须相同。除操作 `5`/`6` 外必须提供非空 `values`。日期范围也可传后端已有的单个预设时间值(例如 `today`、`week`、`month`);不支持的操作、无效日期、倒置边界和缺失值会明确报错。
427
+
407
428
  如需单独核对用户 ID,可调用 `crm_find_user_id`:
408
429
 
409
430
  ```json
package/dist/server.js CHANGED
@@ -13,13 +13,16 @@ const idSchema = z.union([z.string(), z.number()]);
13
13
  const passthroughSchema = z.object({}).passthrough();
14
14
  const toolOutputSchema = z.object({ result: z.unknown() });
15
15
  const searchFilterSchema = z.object({
16
- fieldName: z.string().optional(),
17
- name: z.string().optional(),
18
- formType: z.string().optional(),
19
- type: z.number().optional(),
20
- searchEnum: z.union([z.string(), z.number()]).optional(),
21
- values: z.array(z.unknown()).optional(),
22
- groupId: z.number().optional()
16
+ fieldName: z.string().optional().describe("后端字段名;与兼容字段 name 至少提供一个。"),
17
+ name: z.string().optional().describe("fieldName 的兼容别名。"),
18
+ formType: z.string().optional().describe("字段类型。人员用 user/single_user;日期用 date,日期时间用 datetime。"),
19
+ type: z.number().int().min(1).max(14).optional().describe("高级筛选操作码;与 searchEnum 同义,同时提供时必须一致。人员 ID 匹配用 3,日期范围用 14。"),
20
+ searchEnum: z.union([
21
+ z.number().int().min(1).max(14),
22
+ z.enum(["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14"])
23
+ ]).optional().describe("高级筛选操作码;与 type 同义,同时提供时必须一致。"),
24
+ values: z.array(z.unknown()).optional().describe("筛选值数组。除为空/不为空操作 5/6 外必须提供;datetime 范围 14 使用 [YYYY-MM-DD HH:mm:ss, YYYY-MM-DD HH:mm:ss]。"),
25
+ groupId: z.number().int().optional().describe("同组条件为 AND,不同组为 OR。")
23
26
  }).passthrough();
24
27
  const crmCreateRecordSchema = z.object({
25
28
  module: moduleSchema,
@@ -32,16 +35,16 @@ const crmCreateRecordSchema = z.object({
32
35
  confirm: z.boolean().optional()
33
36
  }).strict();
34
37
  const crmUserSearchShape = {
35
- ownerUserId: idSchema.optional(),
36
- ownerUserIds: z.array(idSchema).optional(),
37
- ownerUserName: z.string().optional(),
38
- ownerUserNames: z.array(z.string()).optional(),
39
- createUserId: idSchema.optional(),
40
- createUserIds: z.array(idSchema).optional(),
41
- createUserName: z.string().optional(),
42
- createUserNames: z.array(z.string()).optional(),
43
- filters: z.array(searchFilterSchema).optional(),
44
- searchList: z.array(searchFilterSchema).optional()
38
+ ownerUserId: idSchema.optional().describe("负责人用户 ID;MCP 转为后端支持的人员包含操作 3。"),
39
+ ownerUserIds: z.array(idSchema).optional().describe("多个负责人用户 ID。"),
40
+ ownerUserName: z.string().optional().describe("负责人唯一昵称;MCP 先解析为用户 ID,重名时报错。"),
41
+ ownerUserNames: z.array(z.string()).optional().describe("多个负责人唯一昵称。"),
42
+ createUserId: idSchema.optional().describe("创建人用户 ID;查询跟进记录时表示跟进人员。"),
43
+ createUserIds: z.array(idSchema).optional().describe("多个创建人用户 ID。"),
44
+ createUserName: z.string().optional().describe("创建人唯一昵称;查询跟进记录时表示跟进人员。"),
45
+ createUserNames: z.array(z.string()).optional().describe("多个创建人唯一昵称。"),
46
+ filters: z.array(searchFilterSchema).optional().describe("高级筛选列表;与 searchList 二选一。"),
47
+ searchList: z.array(searchFilterSchema).optional().describe("filters 的兼容别名;不能与 filters 同时提供。")
45
48
  };
46
49
  const toolDefinitions = [
47
50
  ["crm_auth_status", "使用 CRM_API_KEY 自动换取 Admin-Token 后检查当前登录态,异常时提示检查 API Key。", z.object({}).optional()],
@@ -49,7 +52,7 @@ const toolDefinitions = [
49
52
  ["crm_list_customer_pools", "查询当前账号有权限访问的客户公海,并统计每个公海下面有多少客户;只读,不领取、不分配、不流转。", z.object({ includeSamples: z.boolean().optional(), sampleLimit: z.number().optional() }).passthrough()],
50
53
  ["crm_get_module_schema", "获取模块字段结构和列表头,供本地 AI 判断字段怎么填。", z.object({ module: moduleSchema, id: z.union([z.string(), z.number()]).optional() }).passthrough()],
51
54
  ["crm_validate_field", "调用固定字段校验接口,校验字段值或联系人查重。", passthroughSchema],
52
- ["crm_search_records", "查询模块分页列表;支持 ownerUserId(s)/createUserId(s) 按用户 ID 筛选,也支持 ownerUserName(s)/createUserName(s) 将唯一昵称解析为用户 ID;用户类型 filters 的昵称值同样会先解析为 ID。", z.object({ module: moduleSchema, page: z.number().optional(), limit: z.number().optional(), ...crmUserSearchShape }).passthrough()],
55
+ ["crm_search_records", "查询模块分页列表。人员便捷参数和 user 筛选会解析为用户 ID,并使用后端支持的操作 3;日期/日期时间范围筛选必须使用操作 14。查询某人员某天的跟进记录时,module=activity,人员使用 createUserId(s)/createUserName(s),createTime 使用 formType=datetime 和当天 00:00:00 至 23:59:59 的范围。", z.object({ module: moduleSchema, page: z.number().optional(), limit: z.number().optional(), ...crmUserSearchShape }).passthrough()],
53
56
  ["crm_get_record", "按模块和 ID 查询单条 CRM 记录详情。", z.object({ module: moduleSchema, id: z.union([z.string(), z.number()]) }).passthrough()],
54
57
  ["crm_get_record_information", "查询详情页字段展示数据,包含字段中文名和值。", z.object({ module: moduleSchema, id: z.union([z.string(), z.number()]) }).passthrough()],
55
58
  ["crm_get_related_records", "查询固定白名单内的关联数据,例如商机产品、联系人商机、合同回款计划、产品上架列表。", z.object({ module: moduleSchema, id: z.union([z.string(), z.number()]).optional(), relation: z.string() }).passthrough()],
@@ -192,7 +195,7 @@ export function createWukongMcpServer(config = {}) {
192
195
  const { oauth, ...crmConfig } = config;
193
196
  const server = new McpServer({
194
197
  name: "wukong-mcp",
195
- version: "0.2.4"
198
+ version: "0.2.5"
196
199
  }, {
197
200
  instructions: "先使用只读工具确认模块、记录和字段,再执行写入。所有写入必须显式传 confirm=true;不要尝试任意 URL 请求。"
198
201
  });
package/dist/tools.js CHANGED
@@ -1746,6 +1746,10 @@ function extractSavedWorkorderId(data, prepared) {
1746
1746
  return data?.workorderId ?? prepared.body.entity?.workorderId;
1747
1747
  }
1748
1748
  const FIELD_SEARCH_CONTAINS = 3;
1749
+ const FIELD_SEARCH_NOT_CONTAINS = 4;
1750
+ const FIELD_SEARCH_IS_NULL = 5;
1751
+ const FIELD_SEARCH_IS_NOT_NULL = 6;
1752
+ const FIELD_SEARCH_RANGE = 14;
1749
1753
  const QUICK_SEARCH_FIELDS = {
1750
1754
  leads: [
1751
1755
  { fieldName: "leadsName", formType: "text" },
@@ -1822,6 +1826,42 @@ const NAME_FIELD_BY_MODULE = {
1822
1826
  };
1823
1827
  const USER_FILTER_FORM_TYPES = new Set(["user", "single_user"]);
1824
1828
  const USER_FILTER_FIELD_NAMES = new Set(["ownerUserId", "createUserId"]);
1829
+ const DATE_FILTER_FORM_TYPES = new Set(["date", "datetime"]);
1830
+ const NUMBER_RANGE_FORM_TYPES = new Set(["number", "floatnumber", "percent", "field_attention"]);
1831
+ const USER_FILTER_OPERATIONS = new Set([
1832
+ FIELD_SEARCH_CONTAINS,
1833
+ FIELD_SEARCH_NOT_CONTAINS,
1834
+ FIELD_SEARCH_IS_NULL,
1835
+ FIELD_SEARCH_IS_NOT_NULL
1836
+ ]);
1837
+ const DATE_FILTER_OPERATIONS = new Set([1, 2, 5, 6, 7, 8, 9, 10, FIELD_SEARCH_RANGE]);
1838
+ const PRESET_DATE_RANGES = new Set([
1839
+ "year",
1840
+ "lastYear",
1841
+ "quarter",
1842
+ "lastQuarter",
1843
+ "month",
1844
+ "lastMonth",
1845
+ "week",
1846
+ "lastWeek",
1847
+ "today",
1848
+ "yesterday",
1849
+ "nextYear",
1850
+ "firstHalfYear",
1851
+ "nextHalfYear",
1852
+ "nextQuarter",
1853
+ "nextMonth",
1854
+ "nextWeek",
1855
+ "tomorrow",
1856
+ "previous7day",
1857
+ "previous30day",
1858
+ "future7day",
1859
+ "future30day",
1860
+ "recently7day",
1861
+ "recently30day",
1862
+ "recently60day",
1863
+ "recentlyHalfYear"
1864
+ ]);
1825
1865
  const USER_FILTER_ARGUMENTS = [
1826
1866
  {
1827
1867
  fieldName: "ownerUserId",
@@ -1837,9 +1877,13 @@ const USER_FILTER_ARGUMENTS = [
1837
1877
  async function buildResolvedSearchList(client, args, moduleDefinition) {
1838
1878
  const searchList = buildSearchList(args, moduleDefinition);
1839
1879
  const userSearchList = buildConvenienceUserSearchList(args);
1840
- return resolveUserSearchFilters(client, combineSearchLists(searchList, userSearchList), args.companyId);
1880
+ const validatedSearchList = validateSearchFilters(combineSearchLists(searchList, userSearchList));
1881
+ return resolveUserSearchFilters(client, validatedSearchList, args.companyId);
1841
1882
  }
1842
1883
  function buildSearchList(args, moduleDefinition) {
1884
+ if (args.filters !== undefined && args.searchList !== undefined) {
1885
+ throw new Error("filters 和 searchList 不能同时提供,请只使用其中一个高级筛选参数。");
1886
+ }
1843
1887
  const explicitSearchList = toArray(args.filters ?? args.searchList);
1844
1888
  const semanticSearchList = buildSemanticSearchList(args, moduleDefinition);
1845
1889
  return combineSearchLists(explicitSearchList, semanticSearchList);
@@ -1853,12 +1897,126 @@ function buildConvenienceUserSearchList(args) {
1853
1897
  return [{
1854
1898
  fieldName,
1855
1899
  formType: "user",
1856
- type: 1,
1857
- searchEnum: 1,
1900
+ // 旧 CRM 的 EsUtil.userSearch 只有 CONTAINS(3) 才会生成用户 ID terms 查询。
1901
+ type: FIELD_SEARCH_CONTAINS,
1902
+ searchEnum: FIELD_SEARCH_CONTAINS,
1858
1903
  values
1859
1904
  }];
1860
1905
  });
1861
1906
  }
1907
+ function validateSearchFilters(filters) {
1908
+ return filters.map((filter, index) => {
1909
+ if (!isPlainObject(filter)) {
1910
+ throw new Error(`高级筛选第 ${index + 1} 项必须是对象。`);
1911
+ }
1912
+ const fieldName = firstTextValue(filter.fieldName, filter.name);
1913
+ if (!fieldName) {
1914
+ throw new Error(`高级筛选第 ${index + 1} 项请提供 fieldName(或兼容字段 name)。`);
1915
+ }
1916
+ const typeOperation = parseSearchOperation(filter.type, "type", fieldName);
1917
+ const enumOperation = parseSearchOperation(filter.searchEnum, "searchEnum", fieldName);
1918
+ if (typeOperation !== undefined && enumOperation !== undefined && typeOperation !== enumOperation) {
1919
+ throw new Error(`筛选“${fieldName}”的 type=${typeOperation} 与 searchEnum=${enumOperation} 冲突,请保持一致或只提供一个。`);
1920
+ }
1921
+ const operation = enumOperation ?? typeOperation;
1922
+ if (operation === undefined) {
1923
+ throw new Error(`筛选“${fieldName}”请提供 type 或 searchEnum。`);
1924
+ }
1925
+ if (filter.values !== undefined && !Array.isArray(filter.values)) {
1926
+ throw new Error(`筛选“${fieldName}”的 values 必须是数组。`);
1927
+ }
1928
+ const values = valueArray(filter.values);
1929
+ const isNullOperation = operation === FIELD_SEARCH_IS_NULL || operation === FIELD_SEARCH_IS_NOT_NULL;
1930
+ if (!isNullOperation && values.length === 0) {
1931
+ throw new Error(`筛选“${fieldName}”使用操作 ${operation} 时请提供 values。`);
1932
+ }
1933
+ const formType = String(filter.formType ?? "").trim().toLowerCase();
1934
+ const userFilter = USER_FILTER_FORM_TYPES.has(formType) || USER_FILTER_FIELD_NAMES.has(fieldName);
1935
+ if (userFilter && !USER_FILTER_OPERATIONS.has(operation)) {
1936
+ throw new Error(`人员筛选“${fieldName}”使用了后端未实现的操作 ${operation};仅支持 3、4、5、6。按人员 ID 匹配请使用操作 3。`);
1937
+ }
1938
+ if (DATE_FILTER_FORM_TYPES.has(formType)) {
1939
+ validateDateFilter(fieldName, formType, operation, values);
1940
+ }
1941
+ else if (operation === FIELD_SEARCH_RANGE) {
1942
+ if (!NUMBER_RANGE_FORM_TYPES.has(formType)) {
1943
+ throw new Error(`筛选“${fieldName}”的范围操作 14 仅支持 date、datetime 或数字类型 formType。`);
1944
+ }
1945
+ if (values.length !== 2 || values.some((value) => !isFiniteNumberValue(value))) {
1946
+ throw new Error(`数字筛选“${fieldName}”的范围操作 14 必须提供两个有效数字边界值。`);
1947
+ }
1948
+ }
1949
+ // formType 大小写错误会让后端退回文本搜索;这里统一为契约中的小写值。
1950
+ const normalizedFilter = formType ? { ...filter, formType } : filter;
1951
+ return DATE_FILTER_FORM_TYPES.has(formType) && values.length > 0
1952
+ ? { ...normalizedFilter, values: values.map((value) => String(value).trim()) }
1953
+ : normalizedFilter;
1954
+ });
1955
+ }
1956
+ function parseSearchOperation(value, key, fieldName) {
1957
+ if (value === undefined || value === null) {
1958
+ return undefined;
1959
+ }
1960
+ const text = String(value).trim();
1961
+ if (!/^\d+$/.test(text)) {
1962
+ throw new Error(`筛选“${fieldName}”的 ${key} 必须是 1 到 14 的整数操作码。`);
1963
+ }
1964
+ const operation = Number(text);
1965
+ if (!Number.isInteger(operation) || operation < 1 || operation > 14) {
1966
+ throw new Error(`筛选“${fieldName}”的 ${key}=${text} 不受支持;操作码范围为 1 到 14。`);
1967
+ }
1968
+ return operation;
1969
+ }
1970
+ function validateDateFilter(fieldName, formType, operation, values) {
1971
+ if (!DATE_FILTER_OPERATIONS.has(operation)) {
1972
+ throw new Error(`日期筛选“${fieldName}”的操作 ${operation} 不受后端支持;范围筛选请使用操作 14。`);
1973
+ }
1974
+ if (operation === FIELD_SEARCH_IS_NULL || operation === FIELD_SEARCH_IS_NOT_NULL) {
1975
+ return;
1976
+ }
1977
+ if (operation === FIELD_SEARCH_RANGE && values.length === 1) {
1978
+ const preset = typeof values[0] === "string" ? values[0].trim() : "";
1979
+ if (PRESET_DATE_RANGES.has(preset)) {
1980
+ return;
1981
+ }
1982
+ throw new Error(`日期筛选“${fieldName}”的范围操作 14 请提供两个边界值,或单个受支持的预设时间值。`);
1983
+ }
1984
+ const expectedValueCount = operation === FIELD_SEARCH_RANGE ? 2 : 1;
1985
+ if (values.length !== expectedValueCount) {
1986
+ throw new Error(`日期筛选“${fieldName}”的操作 ${operation} 必须提供 ${expectedValueCount} 个时间值。`);
1987
+ }
1988
+ const normalizedValues = values.map((value) => typeof value === "string" ? value.trim() : "");
1989
+ if (normalizedValues.some((value) => !isValidDateValue(value, formType))) {
1990
+ const format = formType === "datetime" ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD";
1991
+ throw new Error(`日期筛选“${fieldName}”的 values 必须使用 ${format} 格式,并且是有效日期。`);
1992
+ }
1993
+ if (operation === FIELD_SEARCH_RANGE && normalizedValues[0] > normalizedValues[1]) {
1994
+ throw new Error(`日期筛选“${fieldName}”的开始值不能晚于结束值。`);
1995
+ }
1996
+ }
1997
+ function isValidDateValue(value, formType) {
1998
+ const match = formType === "datetime"
1999
+ ? /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/.exec(value)
2000
+ : /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
2001
+ if (!match) {
2002
+ return false;
2003
+ }
2004
+ const [, yearText, monthText, dayText, hourText = "0", minuteText = "0", secondText = "0"] = match;
2005
+ const year = Number(yearText);
2006
+ const month = Number(monthText);
2007
+ const day = Number(dayText);
2008
+ const hour = Number(hourText);
2009
+ const minute = Number(minuteText);
2010
+ const second = Number(secondText);
2011
+ const maxDay = month >= 1 && month <= 12 ? new Date(Date.UTC(year, month, 0)).getUTCDate() : 0;
2012
+ return day >= 1 && day <= maxDay && hour <= 23 && minute <= 59 && second <= 59;
2013
+ }
2014
+ function isFiniteNumberValue(value) {
2015
+ if (typeof value === "number") {
2016
+ return Number.isFinite(value);
2017
+ }
2018
+ return typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value));
2019
+ }
1862
2020
  function argumentValues(args, keys) {
1863
2021
  return keys.flatMap((key) => valueArray(args[key]));
1864
2022
  }
@@ -1922,7 +2080,9 @@ async function resolveUserSearchFilters(client, searchList, companyId) {
1922
2080
  }
1923
2081
  resolvedFilters.push({
1924
2082
  ...filter,
1925
- formType: filter.formType ?? "user",
2083
+ formType: USER_FILTER_FORM_TYPES.has(String(filter.formType ?? "").trim().toLowerCase())
2084
+ ? String(filter.formType).trim().toLowerCase()
2085
+ : "user",
1926
2086
  values: [...new Set(values)]
1927
2087
  });
1928
2088
  }
@@ -2051,7 +2211,7 @@ function toArray(value) {
2051
2211
  return [];
2052
2212
  }
2053
2213
  async function resolveUniqueRecord(client, moduleDefinition, args) {
2054
- const searchList = await resolveUserSearchFilters(client, buildSearchList(args, moduleDefinition), args.companyId);
2214
+ const searchList = await resolveUserSearchFilters(client, validateSearchFilters(buildSearchList(args, moduleDefinition)), args.companyId);
2055
2215
  if (searchList.length === 0) {
2056
2216
  throw new Error(`请提供可定位${moduleDefinition.name}的 customerId/id、phone/mobile、name/customerName 或 keyword。`);
2057
2217
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wukongcrm/mcp-server",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Local and remote MCP server for fixed 72CRM API access.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",