@wukongcrm/mcp-server 0.2.3 → 0.2.4

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
@@ -147,7 +147,7 @@ npm pack
147
147
  生成文件示例:
148
148
 
149
149
  ```text
150
- wukongcrm-mcp-server-0.2.3.tgz
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.3.tgz
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
 
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);必须 confirm=true。",
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
@@ -192,7 +192,7 @@ export function createWukongMcpServer(config = {}) {
192
192
  const { oauth, ...crmConfig } = config;
193
193
  const server = new McpServer({
194
194
  name: "wukong-mcp",
195
- version: "0.2.3"
195
+ version: "0.2.4"
196
196
  }, {
197
197
  instructions: "先使用只读工具确认模块、记录和字段,再执行写入。所有写入必须显式传 confirm=true;不要尝试任意 URL 请求。"
198
198
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wukongcrm/mcp-server",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Local and remote MCP server for fixed 72CRM API access.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",