qingflow-mcp 0.3.23 → 0.3.24

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.
Files changed (3) hide show
  1. package/README.md +33 -5
  2. package/dist/server.js +614 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,6 +13,7 @@ This MCP server wraps Qingflow OpenAPI for:
13
13
  - `qf_form_get`
14
14
  - `qf_field_resolve`
15
15
  - `qf_query_plan`
16
+ - `qf_write_plan`
16
17
  - `qf_records_list`
17
18
  - `qf_record_get`
18
19
  - `qf_records_batch_get`
@@ -117,7 +118,7 @@ npm i -g git+https://github.com/853046310/qingflow-mcp.git
117
118
  Install from npm (pinned version):
118
119
 
119
120
  ```bash
120
- npm i -g qingflow-mcp@0.3.23
121
+ npm i -g qingflow-mcp@0.3.24
121
122
  ```
122
123
 
123
124
  Or one-click installer:
@@ -154,8 +155,9 @@ MCP client config example:
154
155
 
155
156
  1. `qf_apps_list` to pick app.
156
157
  2. `qf_form_get` to inspect field ids/titles and `field_summaries[].write_format`.
157
- 3. `qf_record_create` or `qf_record_update`.
158
- 4. If create/update returns only `request_id`, call `qf_operation_get` to resolve async result.
158
+ 3. For complex forms, run `qf_write_plan` first to surface static blockers and linked-field risks.
159
+ 4. `qf_record_create` or `qf_record_update`.
160
+ 5. If create/update returns only `request_id`, call `qf_operation_get` to resolve async result.
159
161
 
160
162
  Directory / org flow:
161
163
 
@@ -182,7 +184,7 @@ Full calling contract (Chinese):
182
184
 
183
185
  ## Write Format Discovery
184
186
 
185
- For create/update, `0.3.23` now makes special write formats explicit:
187
+ For create/update, `0.3.24` now makes special write formats explicit:
186
188
 
187
189
  1. `qf_form_get`
188
190
  - `field_summaries[].write_format` is populated for member/department fields.
@@ -190,6 +192,10 @@ For create/update, `0.3.23` now makes special write formats explicit:
190
192
  - `qf_record_create` / `qf_record_update` include member/department examples in `limits.special_field_write_formats`.
191
193
  3. `qf_record_create` / `qf_record_update`
192
194
  - invalid member/department values fail fast with `FIELD_VALUE_FORMAT_ERROR`.
195
+ 4. `qf_write_plan`
196
+ - performs static preflight only
197
+ - detects obvious missing required fields, option-linked fields, readonly/system field writes
198
+ - warns when `questionRelations` means final submit may still fail
193
199
 
194
200
  Examples:
195
201
 
@@ -217,6 +223,28 @@ Invalid examples that will now fail:
217
223
  }
218
224
  ```
219
225
 
226
+ `qf_write_plan` example:
227
+
228
+ ```json
229
+ {
230
+ "app_key": "21b3d559",
231
+ "operation": "create",
232
+ "fields": {
233
+ "客户名称": "测试客户",
234
+ "归属销售": [{ "userId": "u_123", "userName": "张三" }],
235
+ "报销类型": [{ "optionId": 1, "value": "出差" }]
236
+ }
237
+ }
238
+ ```
239
+
240
+ Use it when:
241
+
242
+ 1. the form has conditional required fields
243
+ 2. selected options may reveal linked fields
244
+ 3. you need a static blocker list before actual submit
245
+
246
+ Do not treat `qf_write_plan` as a guarantee of successful submit. It is a static preflight built from OpenAPI-visible form metadata, not the full Qingflow frontend validation engine.
247
+
220
248
  ## Unified Query (`qf_query`)
221
249
 
222
250
  `qf_query` is the recommended read entry for agents.
@@ -466,7 +494,7 @@ If you see runtime errors around `Headers` or missing web APIs:
466
494
  2. Upgrade package to latest:
467
495
 
468
496
  ```bash
469
- npm i -g qingflow-mcp@0.3.23
497
+ npm i -g qingflow-mcp@0.3.24
470
498
  ```
471
499
 
472
500
  3. Verify runtime:
package/dist/server.js CHANGED
@@ -68,7 +68,7 @@ const REQUEST_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_REQUEST_TIMEOUT_MS
68
68
  const EXECUTION_BUDGET_MS = toPositiveInt(process.env.QINGFLOW_EXECUTION_BUDGET_MS) ?? 20000;
69
69
  const WAIT_RESULT_DEFAULT_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_WAIT_RESULT_TIMEOUT_MS) ?? 5000;
70
70
  const WAIT_RESULT_POLL_INTERVAL_MS = toPositiveInt(process.env.QINGFLOW_WAIT_RESULT_POLL_INTERVAL_MS) ?? 500;
71
- const SERVER_VERSION = "0.3.23";
71
+ const SERVER_VERSION = "0.3.24";
72
72
  const MEMBER_QUE_TYPE_KEYWORDS = ["member", "user", "成员", "人员"];
73
73
  const DEPARTMENT_QUE_TYPE_KEYWORDS = ["department", "dept", "部门"];
74
74
  const accessToken = process.env.QINGFLOW_ACCESS_TOKEN;
@@ -1345,6 +1345,108 @@ const queryPlanOutputSchema = z.object({
1345
1345
  generated_at: z.string()
1346
1346
  })
1347
1347
  });
1348
+ const writePlanInputPublicSchema = z.object({
1349
+ operation: z.enum(["create", "update"]).optional(),
1350
+ app_key: publicStringSchema,
1351
+ apply_id: publicFieldSelectorSchema.optional(),
1352
+ user_id: publicStringSchema.optional(),
1353
+ force_refresh_form: z.boolean().optional(),
1354
+ answers: z.array(publicAnswerInputSchema).optional(),
1355
+ fields: z.record(z.unknown()).optional()
1356
+ });
1357
+ const writePlanInputSchema = z.preprocess(normalizeWritePlanInput, z
1358
+ .object({
1359
+ operation: z.enum(["create", "update"]).optional(),
1360
+ app_key: z.string().min(1),
1361
+ apply_id: z.union([z.string().min(1), z.number().int()]).optional(),
1362
+ user_id: z.string().min(1).optional(),
1363
+ force_refresh_form: z.boolean().optional(),
1364
+ answers: z.array(answerInputSchema).optional(),
1365
+ fields: z.record(fieldValueSchema).optional()
1366
+ })
1367
+ .refine((value) => hasWritePayload(value.answers, value.fields), {
1368
+ message: "Either answers or fields is required"
1369
+ }));
1370
+ const writePlanFieldRefSchema = z.object({
1371
+ source: z.enum(["fields", "answers"]),
1372
+ requested: z.string(),
1373
+ resolved: z.boolean(),
1374
+ que_id: z.union([z.string(), z.number(), z.null()]),
1375
+ que_title: z.string().nullable(),
1376
+ que_type: z.unknown().nullable(),
1377
+ required: z.boolean().nullable(),
1378
+ readonly: z.boolean().nullable(),
1379
+ system: z.boolean().nullable(),
1380
+ write_format: fieldSummarySchema.shape.write_format,
1381
+ reason: z.string().nullable()
1382
+ });
1383
+ const writePlanFieldIssueSchema = z.object({
1384
+ que_id: z.union([z.string(), z.number(), z.null()]),
1385
+ que_title: z.string().nullable(),
1386
+ que_type: z.unknown().nullable(),
1387
+ reason: z.string()
1388
+ });
1389
+ const writePlanInvalidFieldSchema = z.object({
1390
+ location: z.string().nullable(),
1391
+ message: z.string(),
1392
+ error_code: z.string().nullable(),
1393
+ field: z
1394
+ .object({
1395
+ que_id: z.union([z.string(), z.number(), z.null()]),
1396
+ que_title: z.string().nullable(),
1397
+ que_type: z.unknown().nullable()
1398
+ })
1399
+ .nullable(),
1400
+ expected_format: fieldSummarySchema.shape.write_format,
1401
+ received_value: z.unknown().optional()
1402
+ });
1403
+ const writePlanOptionLinkSchema = z.object({
1404
+ source_que_id: z.union([z.string(), z.number(), z.null()]),
1405
+ source_que_title: z.string().nullable(),
1406
+ option_id: z.union([z.string(), z.number(), z.null()]),
1407
+ option_value: z.string().nullable(),
1408
+ linked_fields: z.array(z.object({
1409
+ que_id: z.union([z.string(), z.number(), z.null()]),
1410
+ que_title: z.string().nullable(),
1411
+ required: z.boolean().nullable(),
1412
+ provided: z.boolean()
1413
+ })),
1414
+ missing_linked_fields: z.array(z.object({
1415
+ que_id: z.union([z.string(), z.number(), z.null()]),
1416
+ que_title: z.string().nullable(),
1417
+ required: z.boolean().nullable()
1418
+ }))
1419
+ });
1420
+ const writePlanOutputSchema = z.object({
1421
+ ok: z.literal(true),
1422
+ data: z.object({
1423
+ operation: z.enum(["create", "update"]),
1424
+ app_key: z.string(),
1425
+ apply_id: z.union([z.string(), z.number(), z.null()]),
1426
+ normalized_answers: z.array(z.record(z.unknown())),
1427
+ resolved_fields: z.array(writePlanFieldRefSchema),
1428
+ validation: z.object({
1429
+ valid: z.boolean(),
1430
+ missing_required_fields: z.array(writePlanFieldIssueSchema),
1431
+ likely_hidden_required_fields: z.array(writePlanFieldIssueSchema),
1432
+ readonly_or_system_fields: z.array(writePlanFieldIssueSchema),
1433
+ invalid_fields: z.array(writePlanInvalidFieldSchema),
1434
+ warnings: z.array(z.string())
1435
+ }),
1436
+ dependencies: z.object({
1437
+ question_relations_present: z.boolean(),
1438
+ relation_count: z.number().int().nonnegative(),
1439
+ option_links: z.array(writePlanOptionLinkSchema)
1440
+ }),
1441
+ ready_to_submit: z.boolean(),
1442
+ blockers: z.array(z.string()),
1443
+ recommended_next_actions: z.array(z.string())
1444
+ }),
1445
+ meta: z.object({
1446
+ version: z.string(),
1447
+ generated_at: z.string()
1448
+ })
1449
+ });
1348
1450
  const batchGetInputPublicSchema = z
1349
1451
  .object({
1350
1452
  app_key: publicStringSchema,
@@ -2188,6 +2290,25 @@ server.registerTool("qf_query_plan", {
2188
2290
  return errorResult(error);
2189
2291
  }
2190
2292
  });
2293
+ server.registerTool("qf_write_plan", {
2294
+ title: "Qingflow Write Plan",
2295
+ description: "Static preflight for create/update payloads. Resolves fields, normalizes answers, checks obvious required/dependency risks, and warns when questionRelations make runtime validation uncertain.",
2296
+ inputSchema: writePlanInputPublicSchema,
2297
+ outputSchema: writePlanOutputSchema,
2298
+ annotations: {
2299
+ readOnlyHint: true,
2300
+ idempotentHint: true
2301
+ }
2302
+ }, async (args) => {
2303
+ try {
2304
+ const parsedArgs = writePlanInputSchema.parse(args);
2305
+ const payload = await executeWritePlan(parsedArgs);
2306
+ return okResult(payload, `Planned ${payload.data.operation} for ${parsedArgs.app_key}`);
2307
+ }
2308
+ catch (error) {
2309
+ return errorResult(error);
2310
+ }
2311
+ });
2191
2312
  server.registerTool("qf_records_list", {
2192
2313
  title: "Qingflow Records List",
2193
2314
  description: "List records with pagination, filters and sorting.",
@@ -3590,6 +3711,28 @@ function buildToolSpecCatalog() {
3590
3711
  resolve_fields: true
3591
3712
  }
3592
3713
  },
3714
+ {
3715
+ tool: "qf_write_plan",
3716
+ required: ["app_key", "answers or fields"],
3717
+ limits: {
3718
+ operation: "create|update (default create; auto-switches to update when apply_id is present)",
3719
+ input_contract: "strict JSON only; answers must be array and fields must be object",
3720
+ capability_boundary: "Static preflight only. Can detect obvious required/dependency risks, but cannot fully execute runtime visibility/validation rules from Qingflow frontend.",
3721
+ special_field_write_formats: {
3722
+ member_list: [{ userId: "u_123", userName: "张三" }],
3723
+ department_list: [{ deptId: 111, deptName: "销售部" }]
3724
+ }
3725
+ },
3726
+ aliases: {},
3727
+ minimal_example: {
3728
+ app_key: "21b3d559",
3729
+ operation: "create",
3730
+ fields: {
3731
+ 客户名称: "测试客户",
3732
+ 归属销售: [{ userId: "u_123", userName: "张三" }]
3733
+ }
3734
+ }
3735
+ },
3593
3736
  {
3594
3737
  tool: "qf_records_list",
3595
3738
  required: ["app_key", "select_columns"],
@@ -3993,6 +4136,26 @@ function normalizeQueryPlanInput(raw) {
3993
4136
  probe: coerceBooleanLike(normalizedObj.probe)
3994
4137
  };
3995
4138
  }
4139
+ function normalizeWritePlanInput(raw) {
4140
+ const parsedRoot = parseJsonLikeDeep(raw);
4141
+ const obj = asObject(parsedRoot);
4142
+ if (!obj) {
4143
+ return parsedRoot;
4144
+ }
4145
+ const normalizedObj = applyAliases(obj, {
4146
+ ...COMMON_INPUT_ALIASES,
4147
+ applyId: "apply_id",
4148
+ forceRefreshForm: "force_refresh_form",
4149
+ mode: "operation",
4150
+ action: "operation"
4151
+ });
4152
+ return {
4153
+ ...normalizedObj,
4154
+ apply_id: coerceNumberLike(normalizeSelectorInputValue(normalizedObj.apply_id)) ?? coerceStringLike(normalizedObj.apply_id),
4155
+ user_id: coerceStringLike(normalizedObj.user_id),
4156
+ force_refresh_form: coerceBooleanLike(normalizedObj.force_refresh_form)
4157
+ };
4158
+ }
3996
4159
  function normalizeBatchGetInput(raw) {
3997
4160
  const parsedRoot = parseJsonLikeDeep(raw);
3998
4161
  const obj = asObject(parsedRoot);
@@ -5717,6 +5880,456 @@ async function executeQueryPlan(args) {
5717
5880
  }
5718
5881
  };
5719
5882
  }
5883
+ async function executeWritePlan(args) {
5884
+ const operation = args.operation ?? (args.apply_id !== undefined ? "update" : "create");
5885
+ const form = await getFormCached(args.app_key, args.user_id, Boolean(args.force_refresh_form));
5886
+ const formObj = asObject(form.result);
5887
+ const index = buildFieldIndex(form.result);
5888
+ const fields = Array.from(index.byId.values());
5889
+ const resolvedFields = collectWritePlanFieldRefs({
5890
+ fieldsInput: args.fields,
5891
+ answersInput: args.answers,
5892
+ index
5893
+ });
5894
+ const invalidFields = [];
5895
+ const warnings = [];
5896
+ let normalizedAnswers = [];
5897
+ try {
5898
+ normalizedAnswers = resolveAnswers({
5899
+ explicitAnswers: args.answers,
5900
+ fields: args.fields,
5901
+ form: form.result,
5902
+ tool: "qf_write_plan"
5903
+ });
5904
+ }
5905
+ catch (error) {
5906
+ invalidFields.push(...normalizeWritePlanErrors(error));
5907
+ }
5908
+ const providedFieldIds = new Set();
5909
+ for (const ref of resolvedFields) {
5910
+ if (ref.resolved && ref.que_id !== null) {
5911
+ providedFieldIds.add(String(ref.que_id));
5912
+ }
5913
+ }
5914
+ for (const answer of normalizedAnswers) {
5915
+ const queId = answer.queId;
5916
+ if (queId !== undefined && queId !== null) {
5917
+ providedFieldIds.add(String(normalizeQueId(queId)));
5918
+ }
5919
+ }
5920
+ const questionRelations = asArray(formObj?.questionRelations);
5921
+ const relationReferencedFieldIds = extractRelationReferencedFieldIds(questionRelations);
5922
+ const optionLinks = collectWritePlanOptionLinks({
5923
+ normalizedAnswers,
5924
+ index,
5925
+ providedFieldIds
5926
+ });
5927
+ const optionLinkedMissingIds = new Set();
5928
+ for (const item of optionLinks) {
5929
+ for (const linked of item.missing_linked_fields) {
5930
+ if (linked.que_id !== null) {
5931
+ optionLinkedMissingIds.add(String(linked.que_id));
5932
+ }
5933
+ }
5934
+ }
5935
+ const missingRequiredFields = [];
5936
+ const likelyHiddenRequiredFields = [];
5937
+ const readonlyOrSystemFields = [];
5938
+ for (const field of fields) {
5939
+ if (field.queId === undefined || field.queId === null) {
5940
+ continue;
5941
+ }
5942
+ const queId = normalizeQueId(field.queId);
5943
+ const required = extractFieldRequiredFlag(field);
5944
+ const readonly = extractFieldReadonlyFlag(field);
5945
+ const system = extractFieldSystemFlag(field);
5946
+ const provided = providedFieldIds.has(String(queId));
5947
+ if (provided && (readonly === true || system === true)) {
5948
+ readonlyOrSystemFields.push({
5949
+ que_id: queId,
5950
+ que_title: asNullableString(field.queTitle),
5951
+ que_type: field.queType ?? null,
5952
+ reason: readonly === true && system === true
5953
+ ? "field looks readonly and system-managed"
5954
+ : readonly === true
5955
+ ? "field looks readonly"
5956
+ : "field looks system-managed"
5957
+ });
5958
+ }
5959
+ if (provided || required !== true) {
5960
+ continue;
5961
+ }
5962
+ const issue = {
5963
+ que_id: queId,
5964
+ que_title: asNullableString(field.queTitle),
5965
+ que_type: field.queType ?? null,
5966
+ reason: optionLinkedMissingIds.has(String(queId))
5967
+ ? "required field is linked by a selected option but not provided"
5968
+ : relationReferencedFieldIds.has(String(queId))
5969
+ ? "required field participates in questionRelations; it may be runtime-hidden/linked"
5970
+ : "required field not provided"
5971
+ };
5972
+ if (optionLinkedMissingIds.has(String(queId)) ||
5973
+ relationReferencedFieldIds.has(String(queId))) {
5974
+ likelyHiddenRequiredFields.push(issue);
5975
+ }
5976
+ else {
5977
+ missingRequiredFields.push(issue);
5978
+ }
5979
+ }
5980
+ if (questionRelations.length > 0) {
5981
+ warnings.push(`form contains ${questionRelations.length} questionRelations; qf_write_plan can only provide static preflight and cannot fully evaluate runtime visibility/required rules`);
5982
+ }
5983
+ if (optionLinks.some((item) => item.missing_linked_fields.length > 0)) {
5984
+ warnings.push("selected options link to additional fields that are not yet provided");
5985
+ }
5986
+ if (normalizedAnswers.length === 0 && invalidFields.length === 0) {
5987
+ warnings.push("no normalized answers were produced from the current payload");
5988
+ }
5989
+ const blockers = [];
5990
+ if (invalidFields.length > 0) {
5991
+ blockers.push(`invalid fields: ${invalidFields.map((item) => item.message).join("; ")}`);
5992
+ }
5993
+ if (missingRequiredFields.length > 0) {
5994
+ blockers.push(`missing required fields: ${missingRequiredFields
5995
+ .map((item) => item.que_title ?? String(item.que_id ?? "unknown"))
5996
+ .join(", ")}`);
5997
+ }
5998
+ if (likelyHiddenRequiredFields.length > 0) {
5999
+ blockers.push(`linked or runtime-dependent required fields may still be missing: ${likelyHiddenRequiredFields
6000
+ .map((item) => item.que_title ?? String(item.que_id ?? "unknown"))
6001
+ .join(", ")}`);
6002
+ }
6003
+ if (readonlyOrSystemFields.length > 0) {
6004
+ blockers.push(`payload includes readonly/system fields: ${readonlyOrSystemFields
6005
+ .map((item) => item.que_title ?? String(item.que_id ?? "unknown"))
6006
+ .join(", ")}`);
6007
+ }
6008
+ const recommendedNextActions = [
6009
+ "Use qf_form_get to inspect field_summaries and write_format before final submit."
6010
+ ];
6011
+ if (missingRequiredFields.length > 0 || likelyHiddenRequiredFields.length > 0) {
6012
+ recommendedNextActions.push("Fill missing required fields before calling qf_record_create or qf_record_update.");
6013
+ }
6014
+ if (optionLinks.some((item) => item.missing_linked_fields.length > 0)) {
6015
+ recommendedNextActions.push("Provide fields linked by the currently selected options, or change the option values.");
6016
+ }
6017
+ if (invalidFields.some((item) => item.expected_format?.kind === "member_list")) {
6018
+ recommendedNextActions.push("Use qf_users_list or qf_department_users_list to resolve valid userId values.");
6019
+ }
6020
+ if (invalidFields.some((item) => item.expected_format?.kind === "department_list")) {
6021
+ recommendedNextActions.push("Use qf_departments_list to resolve valid deptId values.");
6022
+ }
6023
+ if (questionRelations.length > 0) {
6024
+ recommendedNextActions.push("Even when ready_to_submit=true, final submit can still fail because runtime questionRelations are not fully evaluable via OpenAPI.");
6025
+ }
6026
+ return {
6027
+ ok: true,
6028
+ data: {
6029
+ operation,
6030
+ app_key: args.app_key,
6031
+ apply_id: args.apply_id ?? null,
6032
+ normalized_answers: normalizedAnswers,
6033
+ resolved_fields: resolvedFields,
6034
+ validation: {
6035
+ valid: invalidFields.length === 0 &&
6036
+ missingRequiredFields.length === 0 &&
6037
+ likelyHiddenRequiredFields.length === 0 &&
6038
+ readonlyOrSystemFields.length === 0,
6039
+ missing_required_fields: missingRequiredFields,
6040
+ likely_hidden_required_fields: likelyHiddenRequiredFields,
6041
+ readonly_or_system_fields: readonlyOrSystemFields,
6042
+ invalid_fields: invalidFields,
6043
+ warnings: uniqueStringList(warnings)
6044
+ },
6045
+ dependencies: {
6046
+ question_relations_present: questionRelations.length > 0,
6047
+ relation_count: questionRelations.length,
6048
+ option_links: optionLinks
6049
+ },
6050
+ ready_to_submit: invalidFields.length === 0 &&
6051
+ missingRequiredFields.length === 0 &&
6052
+ likelyHiddenRequiredFields.length === 0 &&
6053
+ readonlyOrSystemFields.length === 0,
6054
+ blockers: uniqueStringList(blockers),
6055
+ recommended_next_actions: uniqueStringList(recommendedNextActions)
6056
+ },
6057
+ meta: {
6058
+ version: SERVER_VERSION,
6059
+ generated_at: new Date().toISOString()
6060
+ }
6061
+ };
6062
+ }
6063
+ function collectWritePlanFieldRefs(params) {
6064
+ const refs = [];
6065
+ for (const fieldKey of Object.keys(params.fieldsInput ?? {})) {
6066
+ refs.push(resolveWritePlanFieldRef({
6067
+ source: "fields",
6068
+ requested: fieldKey,
6069
+ index: params.index
6070
+ }));
6071
+ }
6072
+ for (const item of params.answersInput ?? []) {
6073
+ const requestedRaw = item.que_id ?? item.queId ?? item.que_title ?? item.queTitle;
6074
+ if (requestedRaw === undefined || requestedRaw === null) {
6075
+ continue;
6076
+ }
6077
+ refs.push(resolveWritePlanFieldRef({
6078
+ source: "answers",
6079
+ requested: String(requestedRaw),
6080
+ index: params.index
6081
+ }));
6082
+ }
6083
+ return refs;
6084
+ }
6085
+ function resolveWritePlanFieldRef(params) {
6086
+ const requested = params.requested.trim();
6087
+ let field = null;
6088
+ let reason = null;
6089
+ if (requested) {
6090
+ if (isNumericKey(requested)) {
6091
+ field = params.index.byId.get(String(Number(requested))) ?? null;
6092
+ if (!field) {
6093
+ reason = "field not found in form metadata";
6094
+ }
6095
+ }
6096
+ else {
6097
+ try {
6098
+ field = resolveFieldSelectorStrict({
6099
+ fieldKey: requested,
6100
+ index: params.index,
6101
+ tool: "qf_write_plan",
6102
+ location: `${params.source}.${requested}`
6103
+ });
6104
+ }
6105
+ catch (error) {
6106
+ reason = error instanceof Error ? error.message : String(error);
6107
+ }
6108
+ }
6109
+ }
6110
+ return {
6111
+ source: params.source,
6112
+ requested,
6113
+ resolved: Boolean(field && field.queId !== undefined && field.queId !== null),
6114
+ que_id: field?.queId !== undefined && field.queId !== null ? normalizeQueId(field.queId) : null,
6115
+ que_title: asNullableString(field?.queTitle),
6116
+ que_type: field?.queType ?? null,
6117
+ required: field ? extractFieldRequiredFlag(field) : null,
6118
+ readonly: field ? extractFieldReadonlyFlag(field) : null,
6119
+ system: field ? extractFieldSystemFlag(field) : null,
6120
+ write_format: field ? inferFieldWriteFormat(field) : null,
6121
+ reason
6122
+ };
6123
+ }
6124
+ function normalizeWritePlanErrors(error) {
6125
+ if (error instanceof InputValidationError) {
6126
+ const details = asObject(error.details);
6127
+ const field = asObject(details?.field);
6128
+ return [
6129
+ {
6130
+ location: asNullableString(details?.location),
6131
+ message: error.message,
6132
+ error_code: error.errorCode,
6133
+ field: field
6134
+ ? {
6135
+ que_id: field.que_id ?? null,
6136
+ que_title: asNullableString(field.que_title),
6137
+ que_type: field.que_type ?? null
6138
+ }
6139
+ : null,
6140
+ expected_format: details?.expected_format ?? null,
6141
+ received_value: details?.received_value
6142
+ }
6143
+ ];
6144
+ }
6145
+ if (error instanceof z.ZodError) {
6146
+ return error.issues.map((issue) => ({
6147
+ location: issue.path.length > 0 ? issue.path.join(".") : null,
6148
+ message: issue.message,
6149
+ error_code: "INVALID_ARGUMENTS",
6150
+ field: null,
6151
+ expected_format: null,
6152
+ received_value: undefined
6153
+ }));
6154
+ }
6155
+ return [
6156
+ {
6157
+ location: null,
6158
+ message: error instanceof Error ? error.message : String(error),
6159
+ error_code: "WRITE_PLAN_ERROR",
6160
+ field: null,
6161
+ expected_format: null,
6162
+ received_value: undefined
6163
+ }
6164
+ ];
6165
+ }
6166
+ function collectWritePlanOptionLinks(params) {
6167
+ const links = [];
6168
+ for (const answer of params.normalizedAnswers) {
6169
+ const queId = answer.queId;
6170
+ if (queId === undefined || queId === null) {
6171
+ continue;
6172
+ }
6173
+ const field = params.index.byId.get(String(normalizeQueId(queId)));
6174
+ const fieldObj = asObject(field);
6175
+ const options = asArray(fieldObj?.options);
6176
+ if (options.length === 0) {
6177
+ continue;
6178
+ }
6179
+ const selected = collectSelectedOptionKeys(asArray(answer.values));
6180
+ for (const optionRaw of options) {
6181
+ const option = asObject(optionRaw);
6182
+ if (!option) {
6183
+ continue;
6184
+ }
6185
+ const optionIdRaw = option.optId ?? option.optionId ?? option.id ?? null;
6186
+ const optionId = typeof optionIdRaw === "number" && Number.isFinite(optionIdRaw)
6187
+ ? Math.trunc(optionIdRaw)
6188
+ : (asNullableString(optionIdRaw) ?? null);
6189
+ const optionValue = asNullableString(option.optValue ?? option.value ?? option.label ?? option.name);
6190
+ const isSelected = (optionId !== null && selected.ids.has(String(optionId))) ||
6191
+ (optionValue !== null && selected.values.has(optionValue.trim().toLowerCase()));
6192
+ if (!isSelected) {
6193
+ continue;
6194
+ }
6195
+ const linkedFieldIds = uniqueStringList(asArray(option.linkQueIds)
6196
+ .map((item) => typeof item === "number"
6197
+ ? String(Math.trunc(item))
6198
+ : (asNullableString(item)?.trim() ?? ""))
6199
+ .filter((item) => item.length > 0));
6200
+ if (linkedFieldIds.length === 0) {
6201
+ continue;
6202
+ }
6203
+ const linkedFields = linkedFieldIds.map((fieldId) => {
6204
+ const linked = params.index.byId.get(String(Number(fieldId))) ?? params.index.byId.get(fieldId) ?? null;
6205
+ return {
6206
+ que_id: linked?.queId !== undefined && linked.queId !== null
6207
+ ? normalizeQueId(linked.queId)
6208
+ : (isNumericKey(fieldId) ? Number(fieldId) : fieldId),
6209
+ que_title: asNullableString(linked?.queTitle),
6210
+ required: linked ? extractFieldRequiredFlag(linked) : null,
6211
+ provided: params.providedFieldIds.has(fieldId) || params.providedFieldIds.has(String(Number(fieldId)))
6212
+ };
6213
+ });
6214
+ links.push({
6215
+ source_que_id: normalizeQueId(queId),
6216
+ source_que_title: asNullableString(field?.queTitle),
6217
+ option_id: optionId ?? null,
6218
+ option_value: optionValue,
6219
+ linked_fields: linkedFields,
6220
+ missing_linked_fields: linkedFields
6221
+ .filter((item) => !item.provided)
6222
+ .map((item) => ({
6223
+ que_id: item.que_id,
6224
+ que_title: item.que_title,
6225
+ required: item.required
6226
+ }))
6227
+ });
6228
+ }
6229
+ }
6230
+ return links;
6231
+ }
6232
+ function collectSelectedOptionKeys(values) {
6233
+ const ids = new Set();
6234
+ const texts = new Set();
6235
+ for (const value of values) {
6236
+ if (value === null || value === undefined) {
6237
+ continue;
6238
+ }
6239
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
6240
+ ids.add(String(value));
6241
+ texts.add(String(value).trim().toLowerCase());
6242
+ continue;
6243
+ }
6244
+ const obj = asObject(value);
6245
+ if (!obj) {
6246
+ continue;
6247
+ }
6248
+ for (const candidate of [obj.optionId, obj.optId, obj.id]) {
6249
+ if (candidate === undefined || candidate === null) {
6250
+ continue;
6251
+ }
6252
+ ids.add(String(candidate));
6253
+ }
6254
+ for (const candidate of [obj.value, obj.dataValue, obj.valueStr, obj.label, obj.name]) {
6255
+ const normalized = asNullableString(candidate)?.trim().toLowerCase();
6256
+ if (normalized) {
6257
+ texts.add(normalized);
6258
+ }
6259
+ }
6260
+ }
6261
+ return { ids, values: texts };
6262
+ }
6263
+ function extractRelationReferencedFieldIds(relations) {
6264
+ const ids = new Set();
6265
+ const visit = (value, keyHint = "") => {
6266
+ if (value === null || value === undefined) {
6267
+ return;
6268
+ }
6269
+ if (Array.isArray(value)) {
6270
+ for (const item of value) {
6271
+ visit(item, keyHint);
6272
+ }
6273
+ return;
6274
+ }
6275
+ if (typeof value === "number" && Number.isFinite(value)) {
6276
+ if (keyHint && /queid|linkqueids/i.test(keyHint)) {
6277
+ ids.add(String(Math.trunc(value)));
6278
+ }
6279
+ return;
6280
+ }
6281
+ if (typeof value === "string") {
6282
+ const trimmed = value.trim();
6283
+ if (trimmed && keyHint && /queid|linkqueids/i.test(keyHint)) {
6284
+ ids.add(isNumericKey(trimmed) ? String(Number(trimmed)) : trimmed);
6285
+ }
6286
+ return;
6287
+ }
6288
+ const obj = asObject(value);
6289
+ if (!obj) {
6290
+ return;
6291
+ }
6292
+ for (const [key, child] of Object.entries(obj)) {
6293
+ visit(child, key);
6294
+ }
6295
+ };
6296
+ visit(relations);
6297
+ return ids;
6298
+ }
6299
+ function extractFieldRequiredFlag(field) {
6300
+ const obj = asObject(field);
6301
+ if (!obj) {
6302
+ return null;
6303
+ }
6304
+ return firstBooleanFromKeys(obj, ["required", "isRequired", "verifyRequired", "mustFill", "must_fill"]);
6305
+ }
6306
+ function extractFieldReadonlyFlag(field) {
6307
+ const obj = asObject(field);
6308
+ if (!obj) {
6309
+ return null;
6310
+ }
6311
+ const direct = firstBooleanFromKeys(obj, ["readonly", "readOnly", "isReadonly", "isReadOnly"]);
6312
+ if (direct !== null) {
6313
+ return direct;
6314
+ }
6315
+ const editable = firstBooleanFromKeys(obj, ["editable", "isEditable"]);
6316
+ return editable === null ? null : !editable;
6317
+ }
6318
+ function extractFieldSystemFlag(field) {
6319
+ const obj = asObject(field);
6320
+ if (!obj) {
6321
+ return null;
6322
+ }
6323
+ return firstBooleanFromKeys(obj, ["system", "isSystem", "systemField", "isSystemField"]);
6324
+ }
6325
+ function firstBooleanFromKeys(obj, keys) {
6326
+ for (const key of keys) {
6327
+ if (typeof obj[key] === "boolean") {
6328
+ return obj[key];
6329
+ }
6330
+ }
6331
+ return null;
6332
+ }
5720
6333
  async function executeRecordsBatchGet(args) {
5721
6334
  if (!args.app_key) {
5722
6335
  throw missingRequiredFieldError({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qingflow-mcp",
3
- "version": "0.3.23",
3
+ "version": "0.3.24",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",