qingflow-mcp 0.3.22 → 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 +70 -5
  2. package/dist/server.js +888 -24
  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.22
121
+ npm i -g qingflow-mcp@0.3.24
121
122
  ```
122
123
 
123
124
  Or one-click installer:
@@ -153,9 +154,10 @@ MCP client config example:
153
154
  ## Recommended Flow
154
155
 
155
156
  1. `qf_apps_list` to pick app.
156
- 2. `qf_form_get` to inspect field ids/titles.
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.
157
+ 2. `qf_form_get` to inspect field ids/titles and `field_summaries[].write_format`.
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
 
@@ -180,6 +182,69 @@ Full calling contract (Chinese):
180
182
 
181
183
  - [MCP 调用规范](./docs/MCP_CALLING_SPEC.md)
182
184
 
185
+ ## Write Format Discovery
186
+
187
+ For create/update, `0.3.24` now makes special write formats explicit:
188
+
189
+ 1. `qf_form_get`
190
+ - `field_summaries[].write_format` is populated for member/department fields.
191
+ 2. `qf_tool_spec_get`
192
+ - `qf_record_create` / `qf_record_update` include member/department examples in `limits.special_field_write_formats`.
193
+ 3. `qf_record_create` / `qf_record_update`
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
199
+
200
+ Examples:
201
+
202
+ ```json
203
+ {
204
+ "fields": {
205
+ "归属销售": [
206
+ { "userId": "u_123", "userName": "张三" }
207
+ ],
208
+ "归属部门": [
209
+ { "deptId": 111, "deptName": "销售部" }
210
+ ]
211
+ }
212
+ }
213
+ ```
214
+
215
+ Invalid examples that will now fail:
216
+
217
+ ```json
218
+ {
219
+ "fields": {
220
+ "归属销售": "张三",
221
+ "归属部门": "销售部"
222
+ }
223
+ }
224
+ ```
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
+
183
248
  ## Unified Query (`qf_query`)
184
249
 
185
250
  `qf_query` is the recommended read entry for agents.
@@ -429,7 +494,7 @@ If you see runtime errors around `Headers` or missing web APIs:
429
494
  2. Upgrade package to latest:
430
495
 
431
496
  ```bash
432
- npm i -g qingflow-mcp@latest
497
+ npm i -g qingflow-mcp@0.3.24
433
498
  ```
434
499
 
435
500
  3. Verify runtime:
package/dist/server.js CHANGED
@@ -68,7 +68,9 @@ 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.22";
71
+ const SERVER_VERSION = "0.3.24";
72
+ const MEMBER_QUE_TYPE_KEYWORDS = ["member", "user", "成员", "人员"];
73
+ const DEPARTMENT_QUE_TYPE_KEYWORDS = ["department", "dept", "部门"];
72
74
  const accessToken = process.env.QINGFLOW_ACCESS_TOKEN;
73
75
  const baseUrl = process.env.QINGFLOW_BASE_URL;
74
76
  if (!accessToken) {
@@ -192,6 +194,15 @@ const fieldSummarySchema = z.object({
192
194
  que_id: z.union([z.number(), z.string(), z.null()]),
193
195
  que_title: z.string().nullable(),
194
196
  que_type: z.unknown(),
197
+ write_format: z
198
+ .object({
199
+ kind: z.enum(["member_list", "department_list"]),
200
+ description: z.string(),
201
+ item_shape: z.record(z.string()),
202
+ example: z.array(z.record(z.unknown())),
203
+ resolution_hint: z.string()
204
+ })
205
+ .nullable(),
195
206
  has_sub_fields: z.boolean(),
196
207
  sub_field_count: z.number().int().nonnegative()
197
208
  });
@@ -1334,6 +1345,108 @@ const queryPlanOutputSchema = z.object({
1334
1345
  generated_at: z.string()
1335
1346
  })
1336
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
+ });
1337
1450
  const batchGetInputPublicSchema = z
1338
1451
  .object({
1339
1452
  app_key: publicStringSchema,
@@ -2177,6 +2290,25 @@ server.registerTool("qf_query_plan", {
2177
2290
  return errorResult(error);
2178
2291
  }
2179
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
+ });
2180
2312
  server.registerTool("qf_records_list", {
2181
2313
  title: "Qingflow Records List",
2182
2314
  description: "List records with pagination, filters and sorting.",
@@ -2388,7 +2520,8 @@ server.registerTool("qf_record_create", {
2388
2520
  }, async (args) => {
2389
2521
  try {
2390
2522
  const parsedArgs = createInputSchema.parse(args);
2391
- const form = needsFormResolution(parsedArgs.fields) || Boolean(parsedArgs.force_refresh_form)
2523
+ const shouldFetchForm = hasWritePayload(parsedArgs.answers, parsedArgs.fields) || Boolean(parsedArgs.force_refresh_form);
2524
+ const form = shouldFetchForm
2392
2525
  ? await getFormCached(parsedArgs.app_key, parsedArgs.user_id, Boolean(parsedArgs.force_refresh_form))
2393
2526
  : null;
2394
2527
  const normalizedAnswers = resolveAnswers({
@@ -2464,16 +2597,18 @@ server.registerTool("qf_record_update", {
2464
2597
  }, async (args) => {
2465
2598
  try {
2466
2599
  const parsedArgs = updateInputSchema.parse(args);
2467
- const requiresForm = needsFormResolution(parsedArgs.fields);
2468
- if (requiresForm && !parsedArgs.app_key) {
2600
+ const resolvedAppKey = parsedArgs.app_key ?? getCachedApplyAppKey(parsedArgs.apply_id);
2601
+ const requiresFormByTitle = needsFormResolution(parsedArgs.fields);
2602
+ if (requiresFormByTitle && !resolvedAppKey) {
2469
2603
  throw missingRequiredFieldError({
2470
2604
  field: "app_key",
2471
2605
  tool: "qf_record_update",
2472
2606
  fixHint: "Provide app_key when fields uses title-based keys, or switch fields to numeric que_id."
2473
2607
  });
2474
2608
  }
2475
- const form = requiresForm && parsedArgs.app_key
2476
- ? await getFormCached(parsedArgs.app_key, parsedArgs.user_id, Boolean(parsedArgs.force_refresh_form))
2609
+ const form = (hasWritePayload(parsedArgs.answers, parsedArgs.fields) || Boolean(parsedArgs.force_refresh_form)) &&
2610
+ resolvedAppKey
2611
+ ? await getFormCached(resolvedAppKey, parsedArgs.user_id, Boolean(parsedArgs.force_refresh_form))
2477
2612
  : null;
2478
2613
  const normalizedAnswers = resolveAnswers({
2479
2614
  explicitAnswers: parsedArgs.answers,
@@ -3534,7 +3669,8 @@ function buildToolSpecCatalog() {
3534
3669
  tool: "qf_form_get",
3535
3670
  required: ["app_key"],
3536
3671
  limits: {
3537
- app_key: "required string"
3672
+ app_key: "required string",
3673
+ write_format_hint: "field_summaries[].write_format is populated for member/department fields so agents can discover exact create/update payload shapes before writing."
3538
3674
  },
3539
3675
  aliases: collectAliasHints(["app_key", "user_id", "force_refresh"], {}),
3540
3676
  minimal_example: {
@@ -3575,6 +3711,28 @@ function buildToolSpecCatalog() {
3575
3711
  resolve_fields: true
3576
3712
  }
3577
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
+ },
3578
3736
  {
3579
3737
  tool: "qf_records_list",
3580
3738
  required: ["app_key", "select_columns"],
@@ -3751,7 +3909,11 @@ function buildToolSpecCatalog() {
3751
3909
  write_mode: "Provide either answers[] or fields{}",
3752
3910
  wait_result: "optional boolean; when true, polls qf_operation_get internally and returns resolved apply_id directly",
3753
3911
  wait_timeout_ms: "optional int (max 20000); default 5000ms",
3754
- input_contract: "strict JSON only; answers must be array and fields must be object"
3912
+ input_contract: "strict JSON only; answers must be array and fields must be object",
3913
+ special_field_write_formats: {
3914
+ member_list: [{ userId: "u_123", userName: "张三" }],
3915
+ department_list: [{ deptId: 111, deptName: "销售部" }]
3916
+ }
3755
3917
  },
3756
3918
  aliases: {},
3757
3919
  minimal_example: {
@@ -3769,7 +3931,11 @@ function buildToolSpecCatalog() {
3769
3931
  write_mode: "Provide either answers[] or fields{}",
3770
3932
  wait_result: "optional boolean; when true, polls qf_operation_get internally and returns resolved result directly",
3771
3933
  wait_timeout_ms: "optional int (max 20000); default 5000ms",
3772
- input_contract: "strict JSON only; answers must be array and fields must be object"
3934
+ input_contract: "strict JSON only; answers must be array and fields must be object",
3935
+ special_field_write_formats: {
3936
+ member_list: [{ userId: "u_123", userName: "张三" }],
3937
+ department_list: [{ deptId: 111, deptName: "销售部" }]
3938
+ }
3773
3939
  },
3774
3940
  aliases: {},
3775
3941
  minimal_example: {
@@ -3970,6 +4136,26 @@ function normalizeQueryPlanInput(raw) {
3970
4136
  probe: coerceBooleanLike(normalizedObj.probe)
3971
4137
  };
3972
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
+ }
3973
4159
  function normalizeBatchGetInput(raw) {
3974
4160
  const parsedRoot = parseJsonLikeDeep(raw);
3975
4161
  const obj = asObject(parsedRoot);
@@ -5694,6 +5880,456 @@ async function executeQueryPlan(args) {
5694
5880
  }
5695
5881
  };
5696
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
+ }
5697
6333
  async function executeRecordsBatchGet(args) {
5698
6334
  if (!args.app_key) {
5699
6335
  throw missingRequiredFieldError({
@@ -7533,8 +8169,9 @@ function normalizeRecordItem(raw, includeAnswers) {
7533
8169
  return normalized;
7534
8170
  }
7535
8171
  function resolveAnswers(params) {
7536
- const normalizedFromFields = resolveFieldAnswers(params.fields, params.form, params.tool);
7537
- const normalizedExplicit = normalizeExplicitAnswers(params.explicitAnswers);
8172
+ const index = params.form ? buildFieldIndex(params.form) : null;
8173
+ const normalizedFromFields = resolveFieldAnswers(params.fields, index, params.tool);
8174
+ const normalizedExplicit = normalizeExplicitAnswers(params.explicitAnswers, index, params.tool);
7538
8175
  const merged = new Map();
7539
8176
  for (const answer of normalizedFromFields) {
7540
8177
  merged.set(String(answer.queId), answer);
@@ -7547,12 +8184,12 @@ function resolveAnswers(params) {
7547
8184
  }
7548
8185
  return Array.from(merged.values());
7549
8186
  }
7550
- function normalizeExplicitAnswers(answers) {
8187
+ function normalizeExplicitAnswers(answers, index, tool = "qf_record_create") {
7551
8188
  if (!answers?.length) {
7552
8189
  return [];
7553
8190
  }
7554
8191
  const output = [];
7555
- for (const item of answers) {
8192
+ for (const [itemIndex, item] of answers.entries()) {
7556
8193
  const queId = item.que_id ?? item.queId;
7557
8194
  if (queId === undefined || queId === null || String(queId).trim() === "") {
7558
8195
  throw new Error("answer item requires que_id or queId");
@@ -7578,27 +8215,32 @@ function normalizeExplicitAnswers(answers) {
7578
8215
  if (values === undefined) {
7579
8216
  throw new Error(`answer item ${String(queId)} requires values or table_values`);
7580
8217
  }
7581
- normalized.values = values.map((value) => normalizeAnswerValue(value));
8218
+ const field = resolveExplicitAnswerField(item, index);
8219
+ normalized.values = values.map((value, valueIndex) => normalizeAnswerValue(value, {
8220
+ field,
8221
+ tool,
8222
+ location: `answers[${itemIndex}].values[${valueIndex}]`
8223
+ }));
7582
8224
  output.push(normalized);
7583
8225
  }
7584
8226
  return output;
7585
8227
  }
7586
- function resolveFieldAnswers(fields, form, tool = "qf_record_create") {
8228
+ function resolveFieldAnswers(fields, index, tool = "qf_record_create") {
7587
8229
  const entries = Object.entries(fields ?? {});
7588
8230
  if (entries.length === 0) {
7589
8231
  return [];
7590
8232
  }
7591
- const index = buildFieldIndex(form);
8233
+ const resolvedIndex = index ?? { byId: new Map(), byTitle: new Map() };
7592
8234
  const answers = [];
7593
8235
  for (const [fieldKey, fieldValue] of entries) {
7594
8236
  let field;
7595
8237
  if (isNumericKey(fieldKey)) {
7596
- field = resolveFieldByKey(fieldKey, index);
8238
+ field = resolveFieldByKey(fieldKey, resolvedIndex);
7597
8239
  }
7598
8240
  else {
7599
8241
  field = resolveFieldSelectorStrict({
7600
8242
  fieldKey,
7601
- index,
8243
+ index: resolvedIndex,
7602
8244
  tool,
7603
8245
  location: `fields.${fieldKey}`
7604
8246
  });
@@ -7612,15 +8254,15 @@ function resolveFieldAnswers(fields, form, tool = "qf_record_create") {
7612
8254
  tool,
7613
8255
  location: `fields.${fieldKey}`,
7614
8256
  requested: fieldKey,
7615
- suggestions: buildFieldSuggestions(fieldKey, index)
8257
+ suggestions: buildFieldSuggestions(fieldKey, resolvedIndex)
7616
8258
  }
7617
8259
  });
7618
8260
  }
7619
- answers.push(makeAnswerFromField(field, fieldValue));
8261
+ answers.push(makeAnswerFromField(field, fieldValue, tool));
7620
8262
  }
7621
8263
  return answers;
7622
8264
  }
7623
- function makeAnswerFromField(field, value) {
8265
+ function makeAnswerFromField(field, value, tool = "qf_record_create") {
7624
8266
  const base = {
7625
8267
  queId: field.queId
7626
8268
  };
@@ -7641,7 +8283,11 @@ function makeAnswerFromField(field, value) {
7641
8283
  if ("values" in objectValue) {
7642
8284
  return {
7643
8285
  ...base,
7644
- values: asArray(objectValue.values).map((item) => normalizeAnswerValue(item))
8286
+ values: asArray(objectValue.values).map((item, index) => normalizeAnswerValue(item, {
8287
+ field,
8288
+ tool,
8289
+ location: `fields.${String(field.queTitle ?? field.queId ?? "field")}.values[${index}]`
8290
+ }))
7645
8291
  };
7646
8292
  }
7647
8293
  }
@@ -7654,10 +8300,38 @@ function makeAnswerFromField(field, value) {
7654
8300
  const valueArray = Array.isArray(value) ? value : [value];
7655
8301
  return {
7656
8302
  ...base,
7657
- values: valueArray.map((item) => normalizeAnswerValue(item))
8303
+ values: valueArray.map((item, index) => normalizeAnswerValue(item, {
8304
+ field,
8305
+ tool,
8306
+ location: `fields.${String(field.queTitle ?? field.queId ?? "field")}.values[${index}]`
8307
+ }))
7658
8308
  };
7659
8309
  }
7660
- function normalizeAnswerValue(value) {
8310
+ function resolveExplicitAnswerField(item, index) {
8311
+ if (index) {
8312
+ const rawQueId = item.que_id ?? item.queId;
8313
+ if (rawQueId !== undefined && rawQueId !== null) {
8314
+ const hit = index.byId.get(String(normalizeQueId(rawQueId)));
8315
+ if (hit) {
8316
+ return hit;
8317
+ }
8318
+ }
8319
+ const rawQueTitle = item.que_title ?? item.queTitle;
8320
+ if (typeof rawQueTitle === "string" && rawQueTitle.trim()) {
8321
+ const candidates = index.byTitle.get(rawQueTitle.trim().toLowerCase()) ?? [];
8322
+ if (candidates.length === 1) {
8323
+ return candidates[0];
8324
+ }
8325
+ }
8326
+ }
8327
+ return {
8328
+ queId: item.que_id ?? item.queId,
8329
+ queTitle: item.que_title ?? item.queTitle,
8330
+ queType: item.que_type ?? item.queType
8331
+ };
8332
+ }
8333
+ function normalizeAnswerValue(value, params) {
8334
+ validateWriteValueShape(params?.field ?? null, value, params);
7661
8335
  if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
7662
8336
  return {
7663
8337
  value,
@@ -7666,6 +8340,70 @@ function normalizeAnswerValue(value) {
7666
8340
  }
7667
8341
  return value;
7668
8342
  }
8343
+ function validateWriteValueShape(field, value, params) {
8344
+ const writeFormat = field ? inferFieldWriteFormat(field) : null;
8345
+ if (!writeFormat) {
8346
+ return;
8347
+ }
8348
+ const validatedField = field;
8349
+ if (!validatedField) {
8350
+ return;
8351
+ }
8352
+ if (writeFormat.kind === "member_list" && !isValidMemberWriteValue(value)) {
8353
+ throw fieldValueFormatError({
8354
+ field: validatedField,
8355
+ writeFormat,
8356
+ value,
8357
+ tool: params?.tool,
8358
+ location: params?.location
8359
+ });
8360
+ }
8361
+ if (writeFormat.kind === "department_list" && !isValidDepartmentWriteValue(value)) {
8362
+ throw fieldValueFormatError({
8363
+ field: validatedField,
8364
+ writeFormat,
8365
+ value,
8366
+ tool: params?.tool,
8367
+ location: params?.location
8368
+ });
8369
+ }
8370
+ }
8371
+ function isValidMemberWriteValue(value) {
8372
+ const obj = asObject(value);
8373
+ return Boolean(obj && asNullableString(obj.userId)?.trim());
8374
+ }
8375
+ function isValidDepartmentWriteValue(value) {
8376
+ const obj = asObject(value);
8377
+ if (!obj) {
8378
+ return false;
8379
+ }
8380
+ const deptId = obj.deptId;
8381
+ return ((typeof deptId === "string" && deptId.trim().length > 0) ||
8382
+ (typeof deptId === "number" && Number.isFinite(deptId)));
8383
+ }
8384
+ function fieldValueFormatError(params) {
8385
+ const fieldLabel = asNullableString(params.field.queTitle)?.trim() ||
8386
+ String(params.field.queId ?? "unknown_field");
8387
+ const formatName = params.writeFormat.kind === "member_list" ? "成员字段" : "部门字段";
8388
+ return new InputValidationError({
8389
+ message: `${formatName} "${fieldLabel}" 的写入值格式不正确`,
8390
+ errorCode: "FIELD_VALUE_FORMAT_ERROR",
8391
+ fixHint: params.writeFormat.kind === "member_list"
8392
+ ? '传对象数组,例如 [{"userId":"u_123","userName":"张三"}];不要传纯字符串或 user_id。'
8393
+ : '传对象数组,例如 [{"deptId":111,"deptName":"销售部"}];不要传纯字符串或 dept_id。',
8394
+ details: {
8395
+ tool: params.tool ?? "qf_record_create",
8396
+ location: params.location ?? null,
8397
+ field: {
8398
+ que_id: params.field.queId ?? null,
8399
+ que_title: asNullableString(params.field.queTitle),
8400
+ que_type: params.field.queType ?? null
8401
+ },
8402
+ expected_format: params.writeFormat,
8403
+ received_value: params.value
8404
+ }
8405
+ });
8406
+ }
7669
8407
  function needsFormResolution(fields) {
7670
8408
  const keys = Object.keys(fields ?? {});
7671
8409
  if (!keys.length) {
@@ -7697,11 +8435,95 @@ function extractFieldSummaries(form) {
7697
8435
  que_id: field.queId ?? null,
7698
8436
  que_title: asNullableString(field.queTitle),
7699
8437
  que_type: field.queType,
8438
+ write_format: inferFieldWriteFormat(field),
7700
8439
  has_sub_fields: sub.length > 0,
7701
8440
  sub_field_count: sub.length
7702
8441
  };
7703
8442
  });
7704
8443
  }
8444
+ function inferFieldWriteFormat(field) {
8445
+ const kind = inferFieldWriteFormatKind(field.queType);
8446
+ if (kind === "member_list") {
8447
+ return {
8448
+ kind,
8449
+ description: "Pass one or more member objects in values[]. Each item must contain userId.",
8450
+ item_shape: {
8451
+ userId: "string",
8452
+ userName: "string? (recommended)",
8453
+ name: "string? (accepted alias)"
8454
+ },
8455
+ example: [
8456
+ {
8457
+ userId: "u_123",
8458
+ userName: "张三"
8459
+ }
8460
+ ],
8461
+ resolution_hint: "Use qf_users_list or qf_department_users_list to resolve valid userId values before writing."
8462
+ };
8463
+ }
8464
+ if (kind === "department_list") {
8465
+ return {
8466
+ kind,
8467
+ description: "Pass one or more department objects in values[]. Each item must contain deptId.",
8468
+ item_shape: {
8469
+ deptId: "string|number",
8470
+ deptName: "string? (recommended)",
8471
+ name: "string? (accepted alias)"
8472
+ },
8473
+ example: [
8474
+ {
8475
+ deptId: 111,
8476
+ deptName: "销售部"
8477
+ }
8478
+ ],
8479
+ resolution_hint: "Use qf_departments_list to resolve valid deptId values before writing."
8480
+ };
8481
+ }
8482
+ return null;
8483
+ }
8484
+ function inferFieldWriteFormatKind(queType) {
8485
+ const tokens = collectQueTypeTokens(queType);
8486
+ if (tokens.some((token) => MEMBER_QUE_TYPE_KEYWORDS.some((keyword) => token.includes(keyword)))) {
8487
+ return "member_list";
8488
+ }
8489
+ if (tokens.some((token) => DEPARTMENT_QUE_TYPE_KEYWORDS.some((keyword) => token.includes(keyword)))) {
8490
+ return "department_list";
8491
+ }
8492
+ return null;
8493
+ }
8494
+ function collectQueTypeTokens(queType) {
8495
+ const tokens = new Set();
8496
+ const queue = [queType];
8497
+ while (queue.length > 0) {
8498
+ const current = queue.shift();
8499
+ if (current === null || current === undefined) {
8500
+ continue;
8501
+ }
8502
+ if (typeof current === "string") {
8503
+ const normalized = current.trim().toLowerCase();
8504
+ if (normalized) {
8505
+ tokens.add(normalized);
8506
+ }
8507
+ continue;
8508
+ }
8509
+ if (typeof current === "number" || typeof current === "boolean") {
8510
+ tokens.add(String(current));
8511
+ continue;
8512
+ }
8513
+ if (Array.isArray(current)) {
8514
+ queue.push(...current);
8515
+ continue;
8516
+ }
8517
+ const obj = asObject(current);
8518
+ if (!obj) {
8519
+ continue;
8520
+ }
8521
+ for (const value of Object.values(obj)) {
8522
+ queue.push(value);
8523
+ }
8524
+ }
8525
+ return Array.from(tokens);
8526
+ }
7705
8527
  function buildFieldIndex(form) {
7706
8528
  const byId = new Map();
7707
8529
  const byTitle = new Map();
@@ -8442,6 +9264,48 @@ function buildErrorExampleCalls(params) {
8442
9264
  }
8443
9265
  ];
8444
9266
  }
9267
+ if (errorCode === "FIELD_VALUE_FORMAT_ERROR") {
9268
+ const expectedFormat = asObject(params.details?.expected_format);
9269
+ const kind = asNullableString(expectedFormat?.kind);
9270
+ if (kind === "member_list") {
9271
+ return [
9272
+ {
9273
+ tool: "qf_form_get",
9274
+ arguments: {
9275
+ app_key: appKey
9276
+ },
9277
+ note: "先查看字段 write_format,确认成员字段写入 shape"
9278
+ },
9279
+ {
9280
+ tool: "qf_department_users_list",
9281
+ arguments: {
9282
+ dept_id: 111,
9283
+ fetch_child: false
9284
+ },
9285
+ note: "查询有效成员 userId,再按 [{userId,userName}] 写入"
9286
+ }
9287
+ ];
9288
+ }
9289
+ if (kind === "department_list") {
9290
+ return [
9291
+ {
9292
+ tool: "qf_form_get",
9293
+ arguments: {
9294
+ app_key: appKey
9295
+ },
9296
+ note: "先查看字段 write_format,确认部门字段写入 shape"
9297
+ },
9298
+ {
9299
+ tool: "qf_departments_list",
9300
+ arguments: {
9301
+ keyword: "销售",
9302
+ limit: 20
9303
+ },
9304
+ note: "查询有效部门 deptId,再按 [{deptId,deptName}] 写入"
9305
+ }
9306
+ ];
9307
+ }
9308
+ }
8445
9309
  if (errorCode === "INTERNAL_ERROR" || errorCode === "UNKNOWN_ERROR") {
8446
9310
  return [
8447
9311
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qingflow-mcp",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",