@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 +5 -3
- package/dist/nocode.js +352 -4
- package/dist/server.js +1 -1
- 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
|
|
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
|
@@ -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.
|
|
195
|
+
version: "0.2.4"
|
|
196
196
|
}, {
|
|
197
197
|
instructions: "先使用只读工具确认模块、记录和字段,再执行写入。所有写入必须显式传 confirm=true;不要尝试任意 URL 请求。"
|
|
198
198
|
});
|