@wukongcrm/mcp-server 0.2.3 → 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 +27 -4
- package/dist/nocode.js +352 -4
- package/dist/server.js +22 -19
- package/dist/tools.js +165 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,7 +147,7 @@ npm pack
|
|
|
147
147
|
生成文件示例:
|
|
148
148
|
|
|
149
149
|
```text
|
|
150
|
-
wukongcrm-mcp-server-0.2.
|
|
150
|
+
wukongcrm-mcp-server-0.2.4.tgz
|
|
151
151
|
```
|
|
152
152
|
|
|
153
153
|
这个包只包含运行所需的 `dist`、`README.md` 和 `package.json`,不会携带源码、测试文件或用户 API Key。
|
|
@@ -155,7 +155,7 @@ wukongcrm-mcp-server-0.2.3.tgz
|
|
|
155
155
|
使用者拿到 `.tgz` 后安装:
|
|
156
156
|
|
|
157
157
|
```powershell
|
|
158
|
-
npm install -g .\wukongcrm-mcp-server-0.2.
|
|
158
|
+
npm install -g .\wukongcrm-mcp-server-0.2.4.tgz
|
|
159
159
|
```
|
|
160
160
|
|
|
161
161
|
安装后可以直接用命令启动:
|
|
@@ -189,7 +189,7 @@ wukong-mcp
|
|
|
189
189
|
无代码工具只调用源码中声明的固定接口,不提供任意 URL 透传。写工具保持既有 MCP 语义:未传 `confirm=true` 时返回将要调用的接口、请求体和查询参数;明确确认后才执行。
|
|
190
190
|
|
|
191
191
|
- 应用与模块:`nocode_list_applications`、`nocode_get_application`、`nocode_list_modules`、`nocode_get_module`、`nocode_write_application`、`nocode_write_module`
|
|
192
|
-
- 字段与数据:`nocode_get_module_schema`、`nocode_query_fields`、`nocode_search_records`、`nocode_get_record`、`nocode_query_records`、`nocode_calculate_formula`、`nocode_check_duplicate`、`nocode_write_field`、`nocode_write_record`
|
|
192
|
+
- 字段与数据:`nocode_get_module_schema`、`nocode_query_fields`、`nocode_search_records`、`nocode_get_record`、`nocode_query_records`、`nocode_calculate_formula`、`nocode_check_duplicate`、`nocode_write_field`、`nocode_replace_field_with_relation`、`nocode_write_record`
|
|
193
193
|
- 权限与团队:`nocode_list_team_members`、`nocode_get_permissions`、`nocode_write_team`、`nocode_write_role`
|
|
194
194
|
- 场景与页面:`nocode_list_scenes`、`nocode_list_tabs`、`nocode_get_module_file`、`nocode_write_scene`、`nocode_write_tab`
|
|
195
195
|
- 流程与阶段:`nocode_list_flows`、`nocode_list_stages`、`nocode_list_custom_buttons`、`nocode_query_workflow`、`nocode_write_flow`、`nocode_write_stage`、`nocode_write_custom_button`、`nocode_write_workflow`
|
|
@@ -201,8 +201,10 @@ wukong-mcp
|
|
|
201
201
|
常见保存动作必须使用旧前端对应的聚合 BO,而不是把单个字段或单个值直接作为 `payload`:
|
|
202
202
|
|
|
203
203
|
- 字段保存:`ModuleFieldSaveBO`,至少包含 `moduleId`、实际 `version`、`mainFieldId`/`tempMainFieldId` 和非空 `moduleFieldList`。
|
|
204
|
+
- 普通字段改为数据关联:优先使用 `nocode_replace_field_with_relation`。它会按 `baseVersion` 读取完整字段聚合,阻断主字段、明细字段、已有关系、非空数据和删除依赖,保存草稿后再回读验证;不会自动发布。
|
|
204
205
|
- 记录保存:`ModuleFieldDataSaveBO`,至少包含 `moduleId`、实际 `version` 和非空 `fieldDataList`;字段值使用 `fieldId`、`fieldName`、`name`、`type`、`value`,修改时增加 `dataId`。
|
|
205
206
|
- 新建模块的实际版本通常从 `0` 开始,应先用 `nocode_get_module` 读取并显式传入,不能根据已发布老模块猜测为 `1`。
|
|
207
|
+
- 发布模块时,`nocode_write_module(action=publish)` 必须显式传入预览确认过的最新草稿 `version`;旧后端发布接口虽不接收版本,MCP 会在调用前校验版本并在调用后校验激活状态。
|
|
206
208
|
|
|
207
209
|
文件流仍有意不经 MCP 代理,因此应用导入/导出、Excel 导入/导出、跟进导出、打印 PDF/Word 下载不在工具中;打印内容生成和预览 JSON 接口仍可使用。内部测试接口 `/moduleMQ/send` 也不会暴露。
|
|
208
210
|
|
|
@@ -395,13 +397,34 @@ wukong-mcp
|
|
|
395
397
|
{
|
|
396
398
|
"fieldName": "ownerUserId",
|
|
397
399
|
"formType": "user",
|
|
398
|
-
"type":
|
|
400
|
+
"type": 3,
|
|
399
401
|
"values": ["张三"]
|
|
400
402
|
}
|
|
401
403
|
]
|
|
402
404
|
}
|
|
403
405
|
```
|
|
404
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
|
+
|
|
405
428
|
如需单独核对用户 ID,可调用 `crm_find_user_id`:
|
|
406
429
|
|
|
407
430
|
```json
|
package/dist/nocode.js
CHANGED
|
@@ -246,7 +246,7 @@ export const nocodeToolDefinitions = [
|
|
|
246
246
|
},
|
|
247
247
|
{
|
|
248
248
|
name: "nocode_write_module",
|
|
249
|
-
description: "保存、启停、复制、发布、丢弃草稿、删除模块、保存页面布局或维护 BI Dashboard 模块;action=save 的 payload 使用旧 CRM ModuleSaveBO(常见普通模块包含 applicationId、name、moduleType=1、icon
|
|
249
|
+
description: "保存、启停、复制、发布、丢弃草稿、删除模块、保存页面布局或维护 BI Dashboard 模块;action=save 的 payload 使用旧 CRM ModuleSaveBO(常见普通模块包含 applicationId、name、moduleType=1、icon);action=publish 必须显式传 version,工具会锁定当前最新草稿版本;必须 confirm=true。",
|
|
250
250
|
schema: z.object({
|
|
251
251
|
action: z.enum(["save", "status", "copy", "publish", "discardDraft", "delete", "layout", "deleteCancelledCompanyData", "dashboardSave", "dashboardRemove"]),
|
|
252
252
|
moduleId: idSchema.optional(),
|
|
@@ -265,6 +265,21 @@ export const nocodeToolDefinitions = [
|
|
|
265
265
|
access: "write",
|
|
266
266
|
destructive: true
|
|
267
267
|
},
|
|
268
|
+
{
|
|
269
|
+
name: "nocode_replace_field_with_relation",
|
|
270
|
+
description: "把无代码模块中的一个顶层普通自定义字段语义化替换为单选数据关联字段。工具会校验模块版本、主字段/明细/关联字段限制、当前账号可见数据、字段删除依赖和目标已发布状态;默认只预览,confirm=true 后保存为草稿并回读验证,不自动发布。",
|
|
271
|
+
schema: z.object({
|
|
272
|
+
moduleId: idSchema,
|
|
273
|
+
baseVersion: z.number().int().nonnegative(),
|
|
274
|
+
sourceFieldId: idSchema,
|
|
275
|
+
relationFieldName: z.string().trim().min(1, "关联字段名称不能为空。"),
|
|
276
|
+
targetModuleId: idSchema,
|
|
277
|
+
required: z.boolean().optional(),
|
|
278
|
+
confirm: confirmSchema
|
|
279
|
+
}),
|
|
280
|
+
access: "write",
|
|
281
|
+
destructive: true
|
|
282
|
+
},
|
|
268
283
|
{
|
|
269
284
|
name: "nocode_write_record",
|
|
270
285
|
description: "新增/修改、删除、转移负责人或设置分组;action=save 的 payload 必须是 ModuleFieldDataSaveBO,包含 moduleId、实际 version、fieldDataList;每个字段值应包含 fieldId、fieldName、name、type、value,修改时再带 dataId;必须 confirm=true,否则只返回预览。",
|
|
@@ -542,6 +557,7 @@ export function createNocodeToolHandlers(config = {}, sharedClient) {
|
|
|
542
557
|
fixedSort: "/moduleField/fixed/setFieldSort",
|
|
543
558
|
fixedStyle: "/moduleField/fixed/setFieldStyle"
|
|
544
559
|
}),
|
|
560
|
+
nocode_replace_field_with_relation: async (args) => executeRelationFieldReplacement(client, args),
|
|
545
561
|
nocode_write_record: async (args) => executeRecordWrite(client, args, {
|
|
546
562
|
save: "/moduleFieldData/save",
|
|
547
563
|
delete: "/moduleFieldData/delete",
|
|
@@ -954,6 +970,9 @@ async function executeApplicationWrite(client, args) {
|
|
|
954
970
|
}
|
|
955
971
|
async function executeModuleWrite(client, args) {
|
|
956
972
|
const action = String(args.action);
|
|
973
|
+
if (action === "publish") {
|
|
974
|
+
return publishModuleDraft(client, args);
|
|
975
|
+
}
|
|
957
976
|
let targetPath;
|
|
958
977
|
let body;
|
|
959
978
|
let query;
|
|
@@ -969,9 +988,6 @@ async function executeModuleWrite(client, args) {
|
|
|
969
988
|
case "copy":
|
|
970
989
|
targetPath = `/moduleM/copy/${requiredId(args.moduleId, "moduleId")}/${requiredVersion(args.version)}`;
|
|
971
990
|
break;
|
|
972
|
-
case "publish":
|
|
973
|
-
targetPath = `/moduleM/active/${requiredId(args.moduleId, "moduleId")}`;
|
|
974
|
-
break;
|
|
975
991
|
case "discardDraft":
|
|
976
992
|
targetPath = `/moduleM/discardDraft/${requiredId(args.moduleId, "moduleId")}`;
|
|
977
993
|
break;
|
|
@@ -999,6 +1015,270 @@ async function executeModuleWrite(client, args) {
|
|
|
999
1015
|
}
|
|
1000
1016
|
return confirmedPost(client, args, action, targetPath, body, query);
|
|
1001
1017
|
}
|
|
1018
|
+
async function publishModuleDraft(client, args) {
|
|
1019
|
+
const moduleId = requiredId(args.moduleId, "moduleId");
|
|
1020
|
+
const version = requiredExplicitVersion(args.version, "version");
|
|
1021
|
+
const targetPath = `/moduleM/active/${moduleId}`;
|
|
1022
|
+
const latest = requiredObject(await client.post(`/moduleM/queryById/${moduleId}`, undefined, { isLatest: true }), "无法读取待发布模块的最新版本。");
|
|
1023
|
+
const latestVersion = requiredExplicitVersion(latest.version, "latest.version");
|
|
1024
|
+
// 发布接口本身不接收版本号;必须在调用前锁定调用方确认过的草稿版本,避免误发新草稿。
|
|
1025
|
+
if (latestVersion !== version) {
|
|
1026
|
+
throw new Error(`模块版本已变化:请求发布 version=${version},当前最新 version=${latestVersion}。请重新预览。`);
|
|
1027
|
+
}
|
|
1028
|
+
if (latest.isActive !== false || Number(latest.status) !== 2) {
|
|
1029
|
+
throw new Error(`version=${version} 不是可发布草稿,已拒绝调用无版本参数的发布接口。`);
|
|
1030
|
+
}
|
|
1031
|
+
if (args.confirm !== true) {
|
|
1032
|
+
return {
|
|
1033
|
+
preview: true,
|
|
1034
|
+
message: "发布前预览:已锁定当前最新草稿版本;未传 confirm=true,不会调用发布接口。",
|
|
1035
|
+
action: "publish",
|
|
1036
|
+
targetPath,
|
|
1037
|
+
version,
|
|
1038
|
+
preflight: { latestVersion, status: latest.status, isActive: latest.isActive }
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
const published = requiredObject(await client.post(targetPath), "发布接口未返回模块信息,无法确认发布结果。");
|
|
1042
|
+
const publishedVersion = requiredExplicitVersion(published.version, "published.version");
|
|
1043
|
+
const verified = publishedVersion === version && published.isActive === true && Number(published.status) === 1;
|
|
1044
|
+
return {
|
|
1045
|
+
preview: false,
|
|
1046
|
+
action: "publish",
|
|
1047
|
+
targetPath,
|
|
1048
|
+
version,
|
|
1049
|
+
verified,
|
|
1050
|
+
publishReady: verified,
|
|
1051
|
+
message: verified ? "草稿已按确认版本发布,并完成返回值校验。" : "发布接口已调用,但返回值未通过版本/激活状态校验。",
|
|
1052
|
+
data: published
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
async function executeRelationFieldReplacement(client, args) {
|
|
1056
|
+
const moduleId = requiredId(args.moduleId, "moduleId");
|
|
1057
|
+
const sourceFieldId = requiredId(args.sourceFieldId, "sourceFieldId");
|
|
1058
|
+
const targetModuleId = requiredId(args.targetModuleId, "targetModuleId");
|
|
1059
|
+
const baseVersion = requiredExplicitVersion(args.baseVersion, "baseVersion");
|
|
1060
|
+
const relationFieldName = requiredText(args.relationFieldName, "relationFieldName");
|
|
1061
|
+
const moduleDetail = requiredObject(await client.post(`/moduleM/queryById/${moduleId}`, undefined, { isLatest: true }), "无法读取来源模块的最新版本。");
|
|
1062
|
+
const latestVersion = requiredExplicitVersion(moduleDetail.version, "module.version");
|
|
1063
|
+
if (latestVersion !== baseVersion) {
|
|
1064
|
+
throw new Error(`模块版本已变化:baseVersion=${baseVersion},当前最新 version=${latestVersion}。请重新读取结构后再替换。`);
|
|
1065
|
+
}
|
|
1066
|
+
const moduleStatus = Number(moduleDetail.status);
|
|
1067
|
+
if (![1, 2].includes(moduleStatus)
|
|
1068
|
+
|| (moduleStatus === 1 && moduleDetail.isActive !== true)
|
|
1069
|
+
|| (moduleStatus === 2 && moduleDetail.isActive === true)) {
|
|
1070
|
+
throw new Error("来源模块不是可编辑的已发布版本或草稿版本,已拒绝字段替换。");
|
|
1071
|
+
}
|
|
1072
|
+
const mainFieldId = requiredId(moduleDetail.mainFieldId, "module.mainFieldId");
|
|
1073
|
+
const fields = requiredObjectArray(await client.post("/moduleField/queryList", { moduleId: numericValue(moduleId), version: baseVersion }), "模块字段列表");
|
|
1074
|
+
const sourceField = fields.find((field) => sameId(field.fieldId, sourceFieldId));
|
|
1075
|
+
if (!sourceField) {
|
|
1076
|
+
throw new Error(`sourceFieldId=${sourceFieldId} 不在模块 ${moduleId} 的 version=${baseVersion} 字段列表中。`);
|
|
1077
|
+
}
|
|
1078
|
+
// 该动作只负责“普通字段 -> 新建单选数据关联”;现有关系改目标会触发旧后端的非版本化关系配置风险。
|
|
1079
|
+
if (sameId(sourceFieldId, mainFieldId)) {
|
|
1080
|
+
throw new Error("主字段不能被语义化替换。请保留主字段并选择其他普通自定义字段。");
|
|
1081
|
+
}
|
|
1082
|
+
if (Number(sourceField.fieldType) !== 1) {
|
|
1083
|
+
throw new Error("仅支持替换 fieldType=1 的自定义字段,系统字段不会被删除。");
|
|
1084
|
+
}
|
|
1085
|
+
if (sourceField.groupId !== undefined && sourceField.groupId !== null) {
|
|
1086
|
+
throw new Error("明细表格内字段不能被此工具替换;仅支持顶层普通字段。");
|
|
1087
|
+
}
|
|
1088
|
+
// 布局、计算、唯一编号、树和关系字段都有额外配置,不能按普通值字段的删除语义处理。
|
|
1089
|
+
if ([45, 49, 50, 51, 53, 56, 60, 63, 67, 68, 76, 81, 82, 83, 84, 100, 110].includes(Number(sourceField.type))) {
|
|
1090
|
+
throw new Error("布局、计算、配置或已有关系字段不能作为替换来源;请选择顶层普通值字段。");
|
|
1091
|
+
}
|
|
1092
|
+
if ([1, 3].includes(Number(sourceField.operating))) {
|
|
1093
|
+
throw new Error("该字段的 operating 配置不允许删除,已拒绝替换。");
|
|
1094
|
+
}
|
|
1095
|
+
if (fields.some((field) => !sameId(field.fieldId, sourceFieldId) && String(field.name ?? "").trim() === relationFieldName)) {
|
|
1096
|
+
throw new Error(`模块中已存在名为“${relationFieldName}”的其他字段。`);
|
|
1097
|
+
}
|
|
1098
|
+
if (fields.some((field) => Number(field.type) === 68)) {
|
|
1099
|
+
throw new Error("模块包含树字段;旧后端查询结果缺少聚合保存所需的临时展示字段 ID,已保守阻断替换。");
|
|
1100
|
+
}
|
|
1101
|
+
const defaultValueConfigs = requiredObjectArray(await client.post(`/moduleFieldDefault/list/${moduleId}/${baseVersion}`), "字段默认值配置");
|
|
1102
|
+
const fieldStatisticConfigs = requiredObjectArray(await client.post(`/moduleFieldStatistic/query/${moduleId}/${baseVersion}`), "字段统计配置");
|
|
1103
|
+
// /moduleField/save 会先清空再重建这两类配置;语义工具没有完整编辑上下文时宁可拒绝,也不能静默擦除。
|
|
1104
|
+
if (defaultValueConfigs.length > 0 || fieldStatisticConfigs.length > 0) {
|
|
1105
|
+
throw new Error(`模块包含 ${defaultValueConfigs.length} 条字段默认值配置和 ${fieldStatisticConfigs.length} 条字段统计配置,已保守阻断替换。`);
|
|
1106
|
+
}
|
|
1107
|
+
const sourceFieldName = requiredText(sourceField.fieldName, "sourceField.fieldName");
|
|
1108
|
+
const sourceFormType = requiredText(sourceField.formType, "sourceField.formType");
|
|
1109
|
+
const sourceDataQuery = {
|
|
1110
|
+
page: 1,
|
|
1111
|
+
limit: 1,
|
|
1112
|
+
searchList: [{
|
|
1113
|
+
fieldId: numericValue(sourceFieldId),
|
|
1114
|
+
fieldName: sourceFieldName,
|
|
1115
|
+
formType: sourceFormType,
|
|
1116
|
+
type: 6,
|
|
1117
|
+
values: [],
|
|
1118
|
+
groupId: 0
|
|
1119
|
+
}]
|
|
1120
|
+
};
|
|
1121
|
+
const sourceDataPage = await client.post(`/moduleData/queryPageList/${moduleId}`, sourceDataQuery);
|
|
1122
|
+
if (pageHasRows(sourceDataPage)) {
|
|
1123
|
+
throw new Error(`字段“${String(sourceField.name ?? sourceFieldName)}”存在非空数据,替换会在发布时丢失原值,已阻断。`);
|
|
1124
|
+
}
|
|
1125
|
+
const deleteImpact = requiredObject(await client.post("/moduleField/checkFieldDelete", {
|
|
1126
|
+
moduleId: numericValue(moduleId),
|
|
1127
|
+
fieldId: numericValue(sourceFieldId),
|
|
1128
|
+
fieldName: sourceFieldName,
|
|
1129
|
+
version: baseVersion
|
|
1130
|
+
}), "字段删除依赖检查未返回有效结果。");
|
|
1131
|
+
const dependentFields = requiredArray(deleteImpact.moduleFields, "deleteImpact.moduleFields");
|
|
1132
|
+
const dependentFlows = requiredArray(deleteImpact.moduleFlows, "deleteImpact.moduleFlows");
|
|
1133
|
+
if (dependentFields.length > 0 || dependentFlows.length > 0) {
|
|
1134
|
+
throw new Error(`字段仍被 ${dependentFields.length} 个字段配置和 ${dependentFlows.length} 个流程配置引用,已阻断替换。`);
|
|
1135
|
+
}
|
|
1136
|
+
const targetModule = requiredObject(await client.post(`/moduleM/queryById/${targetModuleId}`, undefined, { isLatest: false }), "无法读取关联目标模块的已发布版本。");
|
|
1137
|
+
if (targetModule.isActive !== true || Number(targetModule.status) !== 1) {
|
|
1138
|
+
throw new Error("关联目标模块必须存在已发布激活版本。");
|
|
1139
|
+
}
|
|
1140
|
+
const requiredRelation = args.required === undefined ? Number(sourceField.isNull) === 1 : args.required === true;
|
|
1141
|
+
const retainedFields = fields
|
|
1142
|
+
.filter((field) => !sameId(field.fieldId, sourceFieldId))
|
|
1143
|
+
.map((field) => ({
|
|
1144
|
+
...field,
|
|
1145
|
+
tempFieldId: `existing-${requiredId(field.fieldId, "field.fieldId")}`
|
|
1146
|
+
}));
|
|
1147
|
+
const mainTempFieldId = `existing-${mainFieldId}`;
|
|
1148
|
+
if (!retainedFields.some((field) => field.tempFieldId === mainTempFieldId)) {
|
|
1149
|
+
throw new Error("字段聚合中未找到主字段,拒绝提交可能破坏主字段的保存请求。");
|
|
1150
|
+
}
|
|
1151
|
+
const replacementField = cleanObject({
|
|
1152
|
+
tempFieldId: `relation-${sourceFieldId}-${targetModuleId}`,
|
|
1153
|
+
name: relationFieldName,
|
|
1154
|
+
type: 100,
|
|
1155
|
+
fieldType: 1,
|
|
1156
|
+
isNull: requiredRelation ? 1 : 0,
|
|
1157
|
+
isHidden: 0,
|
|
1158
|
+
formPosition: sourceField.formPosition,
|
|
1159
|
+
stylePercent: sourceField.stylePercent,
|
|
1160
|
+
optionsData: {
|
|
1161
|
+
type: 1,
|
|
1162
|
+
targetName: "module",
|
|
1163
|
+
targetId: numericValue(targetModuleId)
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
const sourceIndex = fields.findIndex((field) => sameId(field.fieldId, sourceFieldId));
|
|
1167
|
+
const moduleFieldList = [...retainedFields];
|
|
1168
|
+
// 旧后端以数组下标重写 sorting;插回来源位置才能保持其他字段与表单的既有顺序。
|
|
1169
|
+
moduleFieldList.splice(sourceIndex, 0, replacementField);
|
|
1170
|
+
const saveBody = {
|
|
1171
|
+
moduleId: numericValue(moduleId),
|
|
1172
|
+
version: baseVersion,
|
|
1173
|
+
mainFieldId: numericValue(mainFieldId),
|
|
1174
|
+
tempMainFieldId: mainTempFieldId,
|
|
1175
|
+
moduleFieldList
|
|
1176
|
+
};
|
|
1177
|
+
const preflight = {
|
|
1178
|
+
latestVersion,
|
|
1179
|
+
sourceDataEmpty: true,
|
|
1180
|
+
dataQueryScope: "当前 MCP 账号可见数据(旧后端强制数据权限过滤)",
|
|
1181
|
+
defaultValueConfigCount: 0,
|
|
1182
|
+
fieldStatisticConfigCount: 0,
|
|
1183
|
+
dependentFieldCount: 0,
|
|
1184
|
+
dependentFlowCount: 0,
|
|
1185
|
+
targetModule: {
|
|
1186
|
+
moduleId: targetModuleId,
|
|
1187
|
+
version: targetModule.version,
|
|
1188
|
+
name: targetModule.name,
|
|
1189
|
+
isActive: targetModule.isActive
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
if (args.confirm !== true) {
|
|
1193
|
+
return {
|
|
1194
|
+
preview: true,
|
|
1195
|
+
message: "字段替换前预览:安全校验已通过;未传 confirm=true,不会保存字段草稿。",
|
|
1196
|
+
action: "replaceFieldWithRelation",
|
|
1197
|
+
targetPath: "/moduleField/save",
|
|
1198
|
+
moduleId,
|
|
1199
|
+
baseVersion,
|
|
1200
|
+
sourceField: fieldSummary(sourceField),
|
|
1201
|
+
replacementField,
|
|
1202
|
+
preflight,
|
|
1203
|
+
body: saveBody
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
const savedModule = requiredObject(await client.post("/moduleField/save", saveBody), "字段保存接口未返回模块信息,无法回读验证。");
|
|
1207
|
+
const savedVersion = requiredExplicitVersion(savedModule.version, "savedModule.version");
|
|
1208
|
+
const expectedSavedVersion = moduleDetail.isActive === true ? baseVersion + 1 : baseVersion;
|
|
1209
|
+
const readbackFields = requiredObjectArray(await client.post("/moduleField/queryList", { moduleId: numericValue(moduleId), version: savedVersion }), "保存后字段列表");
|
|
1210
|
+
const retainedFieldIds = retainedFields.map((field) => requiredId(field.fieldId, "retainedField.fieldId"));
|
|
1211
|
+
const replacementMatches = readbackFields.filter((field) => {
|
|
1212
|
+
const optionsData = isObject(field.optionsData) ? field.optionsData : {};
|
|
1213
|
+
return String(field.name ?? "").trim() === relationFieldName
|
|
1214
|
+
&& Number(field.type) === 100
|
|
1215
|
+
&& Number(field.fieldType) === 1
|
|
1216
|
+
&& Number(optionsData.type) === 1
|
|
1217
|
+
&& optionsData.targetName === "module"
|
|
1218
|
+
&& sameId(optionsData.targetId, targetModuleId);
|
|
1219
|
+
});
|
|
1220
|
+
const verificationErrors = [];
|
|
1221
|
+
if (savedVersion !== expectedSavedVersion) {
|
|
1222
|
+
verificationErrors.push(`保存版本应为 ${expectedSavedVersion},实际为 ${savedVersion}`);
|
|
1223
|
+
}
|
|
1224
|
+
if (Number(savedModule.status) !== 2 || savedModule.isActive !== false) {
|
|
1225
|
+
verificationErrors.push("保存结果不是未激活草稿");
|
|
1226
|
+
}
|
|
1227
|
+
if (readbackFields.some((field) => sameId(field.fieldId, sourceFieldId))) {
|
|
1228
|
+
verificationErrors.push("来源字段仍存在");
|
|
1229
|
+
}
|
|
1230
|
+
if (replacementMatches.length !== 1) {
|
|
1231
|
+
verificationErrors.push(`匹配的新关联字段数量为 ${replacementMatches.length}`);
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
if (Number(replacementMatches[0].isNull) !== (requiredRelation ? 1 : 0)) {
|
|
1235
|
+
verificationErrors.push("新关联字段必填属性不一致");
|
|
1236
|
+
}
|
|
1237
|
+
if (readbackFields.indexOf(replacementMatches[0]) !== sourceIndex) {
|
|
1238
|
+
verificationErrors.push("新关联字段未保留来源字段位置");
|
|
1239
|
+
}
|
|
1240
|
+
if (sourceField.formPosition !== undefined
|
|
1241
|
+
&& sourceField.formPosition !== null
|
|
1242
|
+
&& replacementMatches[0].formPosition !== sourceField.formPosition) {
|
|
1243
|
+
verificationErrors.push("新关联字段表单位置不一致");
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
const missingRetainedIds = retainedFieldIds.filter((fieldId) => !readbackFields.some((field) => sameId(field.fieldId, fieldId)));
|
|
1247
|
+
if (missingRetainedIds.length > 0) {
|
|
1248
|
+
verificationErrors.push(`缺少 ${missingRetainedIds.length} 个应保留字段`);
|
|
1249
|
+
}
|
|
1250
|
+
if (!sameId(savedModule.mainFieldId, mainFieldId)) {
|
|
1251
|
+
verificationErrors.push("主字段 ID 发生变化");
|
|
1252
|
+
}
|
|
1253
|
+
if (readbackFields.length !== fields.length) {
|
|
1254
|
+
verificationErrors.push(`字段总数应为 ${fields.length},实际为 ${readbackFields.length}`);
|
|
1255
|
+
}
|
|
1256
|
+
const verified = verificationErrors.length === 0;
|
|
1257
|
+
const replacement = replacementMatches[0];
|
|
1258
|
+
return {
|
|
1259
|
+
preview: false,
|
|
1260
|
+
action: "replaceFieldWithRelation",
|
|
1261
|
+
targetPath: "/moduleField/save",
|
|
1262
|
+
moduleId,
|
|
1263
|
+
baseVersion,
|
|
1264
|
+
savedVersion,
|
|
1265
|
+
saved: true,
|
|
1266
|
+
verified,
|
|
1267
|
+
publishReady: verified,
|
|
1268
|
+
message: verified
|
|
1269
|
+
? "字段已替换为单选数据关联并保存为草稿,回读验证通过;仍需显式发布该 savedVersion。"
|
|
1270
|
+
: "字段草稿已保存,但回读验证未通过;请检查草稿,不要发布。",
|
|
1271
|
+
preflight,
|
|
1272
|
+
verification: {
|
|
1273
|
+
errors: verificationErrors,
|
|
1274
|
+
sourceRemoved: !readbackFields.some((field) => sameId(field.fieldId, sourceFieldId)),
|
|
1275
|
+
retainedFieldsPresent: missingRetainedIds.length === 0,
|
|
1276
|
+
replacementFieldId: replacement?.fieldId,
|
|
1277
|
+
replacementTargetModuleId: targetModuleId
|
|
1278
|
+
},
|
|
1279
|
+
data: savedModule
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1002
1282
|
async function executePayloadWrite(client, args, paths) {
|
|
1003
1283
|
const action = String(args.action);
|
|
1004
1284
|
const targetPath = paths[action];
|
|
@@ -1116,6 +1396,16 @@ function requiredVersion(value) {
|
|
|
1116
1396
|
}
|
|
1117
1397
|
return normalized;
|
|
1118
1398
|
}
|
|
1399
|
+
function requiredExplicitVersion(value, field) {
|
|
1400
|
+
if (value === undefined || value === null || value === "") {
|
|
1401
|
+
throw new Error(`${field} 必须显式传入非负整数。`);
|
|
1402
|
+
}
|
|
1403
|
+
const normalized = Number(value);
|
|
1404
|
+
if (!Number.isInteger(normalized) || normalized < 0) {
|
|
1405
|
+
throw new Error(`${field} 必须是非负整数。`);
|
|
1406
|
+
}
|
|
1407
|
+
return normalized;
|
|
1408
|
+
}
|
|
1119
1409
|
function requiredInteger(value, field) {
|
|
1120
1410
|
const normalized = Number(value);
|
|
1121
1411
|
if (!Number.isInteger(normalized)) {
|
|
@@ -1148,6 +1438,64 @@ function safePayload(value) {
|
|
|
1148
1438
|
}
|
|
1149
1439
|
return value;
|
|
1150
1440
|
}
|
|
1441
|
+
function isObject(value) {
|
|
1442
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1443
|
+
}
|
|
1444
|
+
function requiredObject(value, message) {
|
|
1445
|
+
if (!isObject(value)) {
|
|
1446
|
+
throw new Error(message);
|
|
1447
|
+
}
|
|
1448
|
+
return value;
|
|
1449
|
+
}
|
|
1450
|
+
function requiredArray(value, field) {
|
|
1451
|
+
if (!Array.isArray(value)) {
|
|
1452
|
+
throw new Error(`${field} 必须是数组;无法完成安全校验。`);
|
|
1453
|
+
}
|
|
1454
|
+
return value;
|
|
1455
|
+
}
|
|
1456
|
+
function requiredObjectArray(value, field) {
|
|
1457
|
+
return requiredArray(value, field).map((item, index) => requiredObject(item, `${field}[${index}] 不是有效对象。`));
|
|
1458
|
+
}
|
|
1459
|
+
function requiredText(value, field) {
|
|
1460
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
1461
|
+
if (!normalized) {
|
|
1462
|
+
throw new Error(`${field} 不能为空。`);
|
|
1463
|
+
}
|
|
1464
|
+
return normalized;
|
|
1465
|
+
}
|
|
1466
|
+
function sameId(value, expected) {
|
|
1467
|
+
return value !== undefined && value !== null && String(value).trim() === expected;
|
|
1468
|
+
}
|
|
1469
|
+
function pageHasRows(value) {
|
|
1470
|
+
const page = requiredObject(value, "字段存量数据检查未返回有效分页结果,已保守阻断。");
|
|
1471
|
+
const rows = Array.isArray(page.list) ? page.list : undefined;
|
|
1472
|
+
if (rows && rows.length > 0) {
|
|
1473
|
+
return true;
|
|
1474
|
+
}
|
|
1475
|
+
if (page.totalRow !== undefined && page.totalRow !== null) {
|
|
1476
|
+
const totalRow = Number(page.totalRow);
|
|
1477
|
+
if (!Number.isInteger(totalRow) || totalRow < 0) {
|
|
1478
|
+
throw new Error("字段存量数据检查返回了无效 totalRow,已保守阻断。");
|
|
1479
|
+
}
|
|
1480
|
+
return totalRow > 0;
|
|
1481
|
+
}
|
|
1482
|
+
if (rows) {
|
|
1483
|
+
return false;
|
|
1484
|
+
}
|
|
1485
|
+
throw new Error("字段存量数据检查既没有 list 也没有 totalRow,已保守阻断。");
|
|
1486
|
+
}
|
|
1487
|
+
function fieldSummary(field) {
|
|
1488
|
+
return cleanObject({
|
|
1489
|
+
fieldId: field.fieldId,
|
|
1490
|
+
fieldName: field.fieldName,
|
|
1491
|
+
name: field.name,
|
|
1492
|
+
type: field.type,
|
|
1493
|
+
formType: field.formType,
|
|
1494
|
+
isNull: field.isNull,
|
|
1495
|
+
formPosition: field.formPosition,
|
|
1496
|
+
stylePercent: field.stylePercent
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1151
1499
|
function applicationPayload(value, action) {
|
|
1152
1500
|
const payload = safePayload(value);
|
|
1153
1501
|
const name = typeof payload.name === "string" ? payload.name.trim() : "";
|
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([
|
|
21
|
-
|
|
22
|
-
|
|
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", "
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1857
|
-
|
|
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 ?? "
|
|
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
|
}
|