qingflow-mcp 0.3.21 → 0.3.23

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 +40 -3
  2. package/dist/server.js +310 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -117,7 +117,7 @@ npm i -g git+https://github.com/853046310/qingflow-mcp.git
117
117
  Install from npm (pinned version):
118
118
 
119
119
  ```bash
120
- npm i -g qingflow-mcp@0.3.21
120
+ npm i -g qingflow-mcp@0.3.23
121
121
  ```
122
122
 
123
123
  Or one-click installer:
@@ -153,7 +153,7 @@ MCP client config example:
153
153
  ## Recommended Flow
154
154
 
155
155
  1. `qf_apps_list` to pick app.
156
- 2. `qf_form_get` to inspect field ids/titles.
156
+ 2. `qf_form_get` to inspect field ids/titles and `field_summaries[].write_format`.
157
157
  3. `qf_record_create` or `qf_record_update`.
158
158
  4. If create/update returns only `request_id`, call `qf_operation_get` to resolve async result.
159
159
 
@@ -180,6 +180,43 @@ Full calling contract (Chinese):
180
180
 
181
181
  - [MCP 调用规范](./docs/MCP_CALLING_SPEC.md)
182
182
 
183
+ ## Write Format Discovery
184
+
185
+ For create/update, `0.3.23` now makes special write formats explicit:
186
+
187
+ 1. `qf_form_get`
188
+ - `field_summaries[].write_format` is populated for member/department fields.
189
+ 2. `qf_tool_spec_get`
190
+ - `qf_record_create` / `qf_record_update` include member/department examples in `limits.special_field_write_formats`.
191
+ 3. `qf_record_create` / `qf_record_update`
192
+ - invalid member/department values fail fast with `FIELD_VALUE_FORMAT_ERROR`.
193
+
194
+ Examples:
195
+
196
+ ```json
197
+ {
198
+ "fields": {
199
+ "归属销售": [
200
+ { "userId": "u_123", "userName": "张三" }
201
+ ],
202
+ "归属部门": [
203
+ { "deptId": 111, "deptName": "销售部" }
204
+ ]
205
+ }
206
+ }
207
+ ```
208
+
209
+ Invalid examples that will now fail:
210
+
211
+ ```json
212
+ {
213
+ "fields": {
214
+ "归属销售": "张三",
215
+ "归属部门": "销售部"
216
+ }
217
+ }
218
+ ```
219
+
183
220
  ## Unified Query (`qf_query`)
184
221
 
185
222
  `qf_query` is the recommended read entry for agents.
@@ -429,7 +466,7 @@ If you see runtime errors around `Headers` or missing web APIs:
429
466
  2. Upgrade package to latest:
430
467
 
431
468
  ```bash
432
- npm i -g qingflow-mcp@latest
469
+ npm i -g qingflow-mcp@0.3.23
433
470
  ```
434
471
 
435
472
  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.21";
71
+ const SERVER_VERSION = "0.3.23";
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
  });
@@ -2388,7 +2399,8 @@ server.registerTool("qf_record_create", {
2388
2399
  }, async (args) => {
2389
2400
  try {
2390
2401
  const parsedArgs = createInputSchema.parse(args);
2391
- const form = needsFormResolution(parsedArgs.fields) || Boolean(parsedArgs.force_refresh_form)
2402
+ const shouldFetchForm = hasWritePayload(parsedArgs.answers, parsedArgs.fields) || Boolean(parsedArgs.force_refresh_form);
2403
+ const form = shouldFetchForm
2392
2404
  ? await getFormCached(parsedArgs.app_key, parsedArgs.user_id, Boolean(parsedArgs.force_refresh_form))
2393
2405
  : null;
2394
2406
  const normalizedAnswers = resolveAnswers({
@@ -2464,16 +2476,18 @@ server.registerTool("qf_record_update", {
2464
2476
  }, async (args) => {
2465
2477
  try {
2466
2478
  const parsedArgs = updateInputSchema.parse(args);
2467
- const requiresForm = needsFormResolution(parsedArgs.fields);
2468
- if (requiresForm && !parsedArgs.app_key) {
2479
+ const resolvedAppKey = parsedArgs.app_key ?? getCachedApplyAppKey(parsedArgs.apply_id);
2480
+ const requiresFormByTitle = needsFormResolution(parsedArgs.fields);
2481
+ if (requiresFormByTitle && !resolvedAppKey) {
2469
2482
  throw missingRequiredFieldError({
2470
2483
  field: "app_key",
2471
2484
  tool: "qf_record_update",
2472
2485
  fixHint: "Provide app_key when fields uses title-based keys, or switch fields to numeric que_id."
2473
2486
  });
2474
2487
  }
2475
- const form = requiresForm && parsedArgs.app_key
2476
- ? await getFormCached(parsedArgs.app_key, parsedArgs.user_id, Boolean(parsedArgs.force_refresh_form))
2488
+ const form = (hasWritePayload(parsedArgs.answers, parsedArgs.fields) || Boolean(parsedArgs.force_refresh_form)) &&
2489
+ resolvedAppKey
2490
+ ? await getFormCached(resolvedAppKey, parsedArgs.user_id, Boolean(parsedArgs.force_refresh_form))
2477
2491
  : null;
2478
2492
  const normalizedAnswers = resolveAnswers({
2479
2493
  explicitAnswers: parsedArgs.answers,
@@ -3534,7 +3548,8 @@ function buildToolSpecCatalog() {
3534
3548
  tool: "qf_form_get",
3535
3549
  required: ["app_key"],
3536
3550
  limits: {
3537
- app_key: "required string"
3551
+ app_key: "required string",
3552
+ 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
3553
  },
3539
3554
  aliases: collectAliasHints(["app_key", "user_id", "force_refresh"], {}),
3540
3555
  minimal_example: {
@@ -3751,7 +3766,11 @@ function buildToolSpecCatalog() {
3751
3766
  write_mode: "Provide either answers[] or fields{}",
3752
3767
  wait_result: "optional boolean; when true, polls qf_operation_get internally and returns resolved apply_id directly",
3753
3768
  wait_timeout_ms: "optional int (max 20000); default 5000ms",
3754
- input_contract: "strict JSON only; answers must be array and fields must be object"
3769
+ input_contract: "strict JSON only; answers must be array and fields must be object",
3770
+ special_field_write_formats: {
3771
+ member_list: [{ userId: "u_123", userName: "张三" }],
3772
+ department_list: [{ deptId: 111, deptName: "销售部" }]
3773
+ }
3755
3774
  },
3756
3775
  aliases: {},
3757
3776
  minimal_example: {
@@ -3769,7 +3788,11 @@ function buildToolSpecCatalog() {
3769
3788
  write_mode: "Provide either answers[] or fields{}",
3770
3789
  wait_result: "optional boolean; when true, polls qf_operation_get internally and returns resolved result directly",
3771
3790
  wait_timeout_ms: "optional int (max 20000); default 5000ms",
3772
- input_contract: "strict JSON only; answers must be array and fields must be object"
3791
+ input_contract: "strict JSON only; answers must be array and fields must be object",
3792
+ special_field_write_formats: {
3793
+ member_list: [{ userId: "u_123", userName: "张三" }],
3794
+ department_list: [{ deptId: 111, deptName: "销售部" }]
3795
+ }
3773
3796
  },
3774
3797
  aliases: {},
3775
3798
  minimal_example: {
@@ -3841,7 +3864,7 @@ function normalizeListInput(raw) {
3841
3864
  strict_full: coerceBooleanLike(normalizedObj.strict_full),
3842
3865
  include_answers: coerceBooleanLike(normalizedObj.include_answers),
3843
3866
  output_profile: normalizeOutputProfileInput(normalizedObj.output_profile),
3844
- apply_ids: normalizeIdArrayInput(normalizedObj.apply_ids),
3867
+ apply_ids: normalizeOpaqueIdArrayInput(normalizedObj.apply_ids),
3845
3868
  sort: normalizeSortInput(normalizedObj.sort),
3846
3869
  filters: normalizeFiltersInput(normalizedObj.filters),
3847
3870
  select_columns: normalizeSelectorListInput(selectColumns),
@@ -3858,7 +3881,7 @@ function normalizeRecordGetInput(raw) {
3858
3881
  const selectColumns = normalizedObj.select_columns ?? normalizedObj.keep_columns;
3859
3882
  return {
3860
3883
  ...normalizedObj,
3861
- apply_id: coerceNumberLike(normalizedObj.apply_id),
3884
+ apply_id: normalizeOpaqueIdInput(normalizedObj.apply_id),
3862
3885
  app_key: coerceStringLike(normalizedObj.app_key),
3863
3886
  max_columns: coerceNumberLike(normalizedObj.max_columns),
3864
3887
  select_columns: normalizeSelectorListInput(selectColumns),
@@ -3884,12 +3907,12 @@ function normalizeQueryInput(raw) {
3884
3907
  max_rows: coerceNumberLike(normalizedObj.max_rows),
3885
3908
  max_items: coerceNumberLike(normalizedObj.max_items),
3886
3909
  max_columns: coerceNumberLike(normalizedObj.max_columns),
3887
- apply_id: coerceNumberLike(normalizedObj.apply_id),
3910
+ apply_id: normalizeOpaqueIdInput(normalizedObj.apply_id),
3888
3911
  strict_full: coerceBooleanLike(normalizedObj.strict_full),
3889
3912
  include_answers: coerceBooleanLike(normalizedObj.include_answers),
3890
3913
  output_profile: normalizeOutputProfileInput(normalizedObj.output_profile),
3891
3914
  amount_column: normalizeAmountColumnInput(normalizedObj.amount_column),
3892
- apply_ids: normalizeIdArrayInput(normalizedObj.apply_ids),
3915
+ apply_ids: normalizeOpaqueIdArrayInput(normalizedObj.apply_ids),
3893
3916
  sort: normalizeSortInput(normalizedObj.sort),
3894
3917
  filters: normalizeFiltersInput(normalizedObj.filters),
3895
3918
  select_columns: normalizeSelectorListInput(selectColumns),
@@ -3921,7 +3944,7 @@ function normalizeAggregateInput(raw) {
3921
3944
  amount_column: normalizeAmountColumnInput(amountColumns),
3922
3945
  metrics: normalizeMetricsInput(normalizedObj.metrics),
3923
3946
  time_bucket: normalizeTimeBucketInput(normalizedObj.time_bucket),
3924
- apply_ids: normalizeIdArrayInput(normalizedObj.apply_ids),
3947
+ apply_ids: normalizeOpaqueIdArrayInput(normalizedObj.apply_ids),
3925
3948
  sort: normalizeSortInput(normalizedObj.sort),
3926
3949
  filters: normalizeFiltersInput(normalizedObj.filters),
3927
3950
  time_range: timeRange,
@@ -3981,7 +4004,7 @@ function normalizeBatchGetInput(raw) {
3981
4004
  return {
3982
4005
  ...normalizedObj,
3983
4006
  app_key: coerceStringLike(normalizedObj.app_key),
3984
- apply_ids: normalizeIdArrayInput(normalizedObj.apply_ids),
4007
+ apply_ids: normalizeOpaqueIdArrayInput(normalizedObj.apply_ids),
3985
4008
  max_columns: coerceNumberLike(normalizedObj.max_columns),
3986
4009
  select_columns: normalizeSelectorListInput(selectColumns),
3987
4010
  output_profile: normalizeOutputProfileInput(normalizedObj.output_profile)
@@ -4007,7 +4030,7 @@ function normalizeExportInput(raw) {
4007
4030
  max_columns: coerceNumberLike(normalizedObj.max_columns),
4008
4031
  strict_full: coerceBooleanLike(normalizedObj.strict_full),
4009
4032
  output_profile: normalizeOutputProfileInput(normalizedObj.output_profile),
4010
- apply_ids: normalizeIdArrayInput(normalizedObj.apply_ids),
4033
+ apply_ids: normalizeOpaqueIdArrayInput(normalizedObj.apply_ids),
4011
4034
  sort: normalizeSortInput(normalizedObj.sort),
4012
4035
  filters: normalizeFiltersInput(normalizedObj.filters),
4013
4036
  select_columns: normalizeSelectorListInput(selectColumns),
@@ -4134,6 +4157,34 @@ function normalizeIdArrayInput(value) {
4134
4157
  }
4135
4158
  return parsed;
4136
4159
  }
4160
+ function normalizeOpaqueIdInput(value) {
4161
+ const parsed = parseJsonLikeDeep(value);
4162
+ if (parsed === undefined || parsed === null) {
4163
+ return parsed;
4164
+ }
4165
+ if (typeof parsed === "string") {
4166
+ const trimmed = parsed.trim();
4167
+ return trimmed ? trimmed : parsed;
4168
+ }
4169
+ if (typeof parsed === "number" && Number.isFinite(parsed)) {
4170
+ return String(Math.trunc(parsed));
4171
+ }
4172
+ return parsed;
4173
+ }
4174
+ function normalizeOpaqueIdArrayInput(value) {
4175
+ const parsed = parseJsonLikeDeep(value);
4176
+ if (Array.isArray(parsed)) {
4177
+ return parsed.map((item) => normalizeOpaqueIdInput(item));
4178
+ }
4179
+ if (typeof parsed === "string" && parsed.includes(",")) {
4180
+ return parsed
4181
+ .split(",")
4182
+ .map((item) => item.trim())
4183
+ .filter((item) => item.length > 0)
4184
+ .map((item) => normalizeOpaqueIdInput(item));
4185
+ }
4186
+ return parsed;
4187
+ }
4137
4188
  function normalizeSortInput(value) {
4138
4189
  const parsed = parseJsonLikeDeep(value);
4139
4190
  if (!Array.isArray(parsed)) {
@@ -7505,8 +7556,9 @@ function normalizeRecordItem(raw, includeAnswers) {
7505
7556
  return normalized;
7506
7557
  }
7507
7558
  function resolveAnswers(params) {
7508
- const normalizedFromFields = resolveFieldAnswers(params.fields, params.form, params.tool);
7509
- const normalizedExplicit = normalizeExplicitAnswers(params.explicitAnswers);
7559
+ const index = params.form ? buildFieldIndex(params.form) : null;
7560
+ const normalizedFromFields = resolveFieldAnswers(params.fields, index, params.tool);
7561
+ const normalizedExplicit = normalizeExplicitAnswers(params.explicitAnswers, index, params.tool);
7510
7562
  const merged = new Map();
7511
7563
  for (const answer of normalizedFromFields) {
7512
7564
  merged.set(String(answer.queId), answer);
@@ -7519,12 +7571,12 @@ function resolveAnswers(params) {
7519
7571
  }
7520
7572
  return Array.from(merged.values());
7521
7573
  }
7522
- function normalizeExplicitAnswers(answers) {
7574
+ function normalizeExplicitAnswers(answers, index, tool = "qf_record_create") {
7523
7575
  if (!answers?.length) {
7524
7576
  return [];
7525
7577
  }
7526
7578
  const output = [];
7527
- for (const item of answers) {
7579
+ for (const [itemIndex, item] of answers.entries()) {
7528
7580
  const queId = item.que_id ?? item.queId;
7529
7581
  if (queId === undefined || queId === null || String(queId).trim() === "") {
7530
7582
  throw new Error("answer item requires que_id or queId");
@@ -7550,27 +7602,32 @@ function normalizeExplicitAnswers(answers) {
7550
7602
  if (values === undefined) {
7551
7603
  throw new Error(`answer item ${String(queId)} requires values or table_values`);
7552
7604
  }
7553
- normalized.values = values.map((value) => normalizeAnswerValue(value));
7605
+ const field = resolveExplicitAnswerField(item, index);
7606
+ normalized.values = values.map((value, valueIndex) => normalizeAnswerValue(value, {
7607
+ field,
7608
+ tool,
7609
+ location: `answers[${itemIndex}].values[${valueIndex}]`
7610
+ }));
7554
7611
  output.push(normalized);
7555
7612
  }
7556
7613
  return output;
7557
7614
  }
7558
- function resolveFieldAnswers(fields, form, tool = "qf_record_create") {
7615
+ function resolveFieldAnswers(fields, index, tool = "qf_record_create") {
7559
7616
  const entries = Object.entries(fields ?? {});
7560
7617
  if (entries.length === 0) {
7561
7618
  return [];
7562
7619
  }
7563
- const index = buildFieldIndex(form);
7620
+ const resolvedIndex = index ?? { byId: new Map(), byTitle: new Map() };
7564
7621
  const answers = [];
7565
7622
  for (const [fieldKey, fieldValue] of entries) {
7566
7623
  let field;
7567
7624
  if (isNumericKey(fieldKey)) {
7568
- field = resolveFieldByKey(fieldKey, index);
7625
+ field = resolveFieldByKey(fieldKey, resolvedIndex);
7569
7626
  }
7570
7627
  else {
7571
7628
  field = resolveFieldSelectorStrict({
7572
7629
  fieldKey,
7573
- index,
7630
+ index: resolvedIndex,
7574
7631
  tool,
7575
7632
  location: `fields.${fieldKey}`
7576
7633
  });
@@ -7584,15 +7641,15 @@ function resolveFieldAnswers(fields, form, tool = "qf_record_create") {
7584
7641
  tool,
7585
7642
  location: `fields.${fieldKey}`,
7586
7643
  requested: fieldKey,
7587
- suggestions: buildFieldSuggestions(fieldKey, index)
7644
+ suggestions: buildFieldSuggestions(fieldKey, resolvedIndex)
7588
7645
  }
7589
7646
  });
7590
7647
  }
7591
- answers.push(makeAnswerFromField(field, fieldValue));
7648
+ answers.push(makeAnswerFromField(field, fieldValue, tool));
7592
7649
  }
7593
7650
  return answers;
7594
7651
  }
7595
- function makeAnswerFromField(field, value) {
7652
+ function makeAnswerFromField(field, value, tool = "qf_record_create") {
7596
7653
  const base = {
7597
7654
  queId: field.queId
7598
7655
  };
@@ -7613,7 +7670,11 @@ function makeAnswerFromField(field, value) {
7613
7670
  if ("values" in objectValue) {
7614
7671
  return {
7615
7672
  ...base,
7616
- values: asArray(objectValue.values).map((item) => normalizeAnswerValue(item))
7673
+ values: asArray(objectValue.values).map((item, index) => normalizeAnswerValue(item, {
7674
+ field,
7675
+ tool,
7676
+ location: `fields.${String(field.queTitle ?? field.queId ?? "field")}.values[${index}]`
7677
+ }))
7617
7678
  };
7618
7679
  }
7619
7680
  }
@@ -7626,10 +7687,38 @@ function makeAnswerFromField(field, value) {
7626
7687
  const valueArray = Array.isArray(value) ? value : [value];
7627
7688
  return {
7628
7689
  ...base,
7629
- values: valueArray.map((item) => normalizeAnswerValue(item))
7690
+ values: valueArray.map((item, index) => normalizeAnswerValue(item, {
7691
+ field,
7692
+ tool,
7693
+ location: `fields.${String(field.queTitle ?? field.queId ?? "field")}.values[${index}]`
7694
+ }))
7695
+ };
7696
+ }
7697
+ function resolveExplicitAnswerField(item, index) {
7698
+ if (index) {
7699
+ const rawQueId = item.que_id ?? item.queId;
7700
+ if (rawQueId !== undefined && rawQueId !== null) {
7701
+ const hit = index.byId.get(String(normalizeQueId(rawQueId)));
7702
+ if (hit) {
7703
+ return hit;
7704
+ }
7705
+ }
7706
+ const rawQueTitle = item.que_title ?? item.queTitle;
7707
+ if (typeof rawQueTitle === "string" && rawQueTitle.trim()) {
7708
+ const candidates = index.byTitle.get(rawQueTitle.trim().toLowerCase()) ?? [];
7709
+ if (candidates.length === 1) {
7710
+ return candidates[0];
7711
+ }
7712
+ }
7713
+ }
7714
+ return {
7715
+ queId: item.que_id ?? item.queId,
7716
+ queTitle: item.que_title ?? item.queTitle,
7717
+ queType: item.que_type ?? item.queType
7630
7718
  };
7631
7719
  }
7632
- function normalizeAnswerValue(value) {
7720
+ function normalizeAnswerValue(value, params) {
7721
+ validateWriteValueShape(params?.field ?? null, value, params);
7633
7722
  if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
7634
7723
  return {
7635
7724
  value,
@@ -7638,6 +7727,70 @@ function normalizeAnswerValue(value) {
7638
7727
  }
7639
7728
  return value;
7640
7729
  }
7730
+ function validateWriteValueShape(field, value, params) {
7731
+ const writeFormat = field ? inferFieldWriteFormat(field) : null;
7732
+ if (!writeFormat) {
7733
+ return;
7734
+ }
7735
+ const validatedField = field;
7736
+ if (!validatedField) {
7737
+ return;
7738
+ }
7739
+ if (writeFormat.kind === "member_list" && !isValidMemberWriteValue(value)) {
7740
+ throw fieldValueFormatError({
7741
+ field: validatedField,
7742
+ writeFormat,
7743
+ value,
7744
+ tool: params?.tool,
7745
+ location: params?.location
7746
+ });
7747
+ }
7748
+ if (writeFormat.kind === "department_list" && !isValidDepartmentWriteValue(value)) {
7749
+ throw fieldValueFormatError({
7750
+ field: validatedField,
7751
+ writeFormat,
7752
+ value,
7753
+ tool: params?.tool,
7754
+ location: params?.location
7755
+ });
7756
+ }
7757
+ }
7758
+ function isValidMemberWriteValue(value) {
7759
+ const obj = asObject(value);
7760
+ return Boolean(obj && asNullableString(obj.userId)?.trim());
7761
+ }
7762
+ function isValidDepartmentWriteValue(value) {
7763
+ const obj = asObject(value);
7764
+ if (!obj) {
7765
+ return false;
7766
+ }
7767
+ const deptId = obj.deptId;
7768
+ return ((typeof deptId === "string" && deptId.trim().length > 0) ||
7769
+ (typeof deptId === "number" && Number.isFinite(deptId)));
7770
+ }
7771
+ function fieldValueFormatError(params) {
7772
+ const fieldLabel = asNullableString(params.field.queTitle)?.trim() ||
7773
+ String(params.field.queId ?? "unknown_field");
7774
+ const formatName = params.writeFormat.kind === "member_list" ? "成员字段" : "部门字段";
7775
+ return new InputValidationError({
7776
+ message: `${formatName} "${fieldLabel}" 的写入值格式不正确`,
7777
+ errorCode: "FIELD_VALUE_FORMAT_ERROR",
7778
+ fixHint: params.writeFormat.kind === "member_list"
7779
+ ? '传对象数组,例如 [{"userId":"u_123","userName":"张三"}];不要传纯字符串或 user_id。'
7780
+ : '传对象数组,例如 [{"deptId":111,"deptName":"销售部"}];不要传纯字符串或 dept_id。',
7781
+ details: {
7782
+ tool: params.tool ?? "qf_record_create",
7783
+ location: params.location ?? null,
7784
+ field: {
7785
+ que_id: params.field.queId ?? null,
7786
+ que_title: asNullableString(params.field.queTitle),
7787
+ que_type: params.field.queType ?? null
7788
+ },
7789
+ expected_format: params.writeFormat,
7790
+ received_value: params.value
7791
+ }
7792
+ });
7793
+ }
7641
7794
  function needsFormResolution(fields) {
7642
7795
  const keys = Object.keys(fields ?? {});
7643
7796
  if (!keys.length) {
@@ -7669,11 +7822,95 @@ function extractFieldSummaries(form) {
7669
7822
  que_id: field.queId ?? null,
7670
7823
  que_title: asNullableString(field.queTitle),
7671
7824
  que_type: field.queType,
7825
+ write_format: inferFieldWriteFormat(field),
7672
7826
  has_sub_fields: sub.length > 0,
7673
7827
  sub_field_count: sub.length
7674
7828
  };
7675
7829
  });
7676
7830
  }
7831
+ function inferFieldWriteFormat(field) {
7832
+ const kind = inferFieldWriteFormatKind(field.queType);
7833
+ if (kind === "member_list") {
7834
+ return {
7835
+ kind,
7836
+ description: "Pass one or more member objects in values[]. Each item must contain userId.",
7837
+ item_shape: {
7838
+ userId: "string",
7839
+ userName: "string? (recommended)",
7840
+ name: "string? (accepted alias)"
7841
+ },
7842
+ example: [
7843
+ {
7844
+ userId: "u_123",
7845
+ userName: "张三"
7846
+ }
7847
+ ],
7848
+ resolution_hint: "Use qf_users_list or qf_department_users_list to resolve valid userId values before writing."
7849
+ };
7850
+ }
7851
+ if (kind === "department_list") {
7852
+ return {
7853
+ kind,
7854
+ description: "Pass one or more department objects in values[]. Each item must contain deptId.",
7855
+ item_shape: {
7856
+ deptId: "string|number",
7857
+ deptName: "string? (recommended)",
7858
+ name: "string? (accepted alias)"
7859
+ },
7860
+ example: [
7861
+ {
7862
+ deptId: 111,
7863
+ deptName: "销售部"
7864
+ }
7865
+ ],
7866
+ resolution_hint: "Use qf_departments_list to resolve valid deptId values before writing."
7867
+ };
7868
+ }
7869
+ return null;
7870
+ }
7871
+ function inferFieldWriteFormatKind(queType) {
7872
+ const tokens = collectQueTypeTokens(queType);
7873
+ if (tokens.some((token) => MEMBER_QUE_TYPE_KEYWORDS.some((keyword) => token.includes(keyword)))) {
7874
+ return "member_list";
7875
+ }
7876
+ if (tokens.some((token) => DEPARTMENT_QUE_TYPE_KEYWORDS.some((keyword) => token.includes(keyword)))) {
7877
+ return "department_list";
7878
+ }
7879
+ return null;
7880
+ }
7881
+ function collectQueTypeTokens(queType) {
7882
+ const tokens = new Set();
7883
+ const queue = [queType];
7884
+ while (queue.length > 0) {
7885
+ const current = queue.shift();
7886
+ if (current === null || current === undefined) {
7887
+ continue;
7888
+ }
7889
+ if (typeof current === "string") {
7890
+ const normalized = current.trim().toLowerCase();
7891
+ if (normalized) {
7892
+ tokens.add(normalized);
7893
+ }
7894
+ continue;
7895
+ }
7896
+ if (typeof current === "number" || typeof current === "boolean") {
7897
+ tokens.add(String(current));
7898
+ continue;
7899
+ }
7900
+ if (Array.isArray(current)) {
7901
+ queue.push(...current);
7902
+ continue;
7903
+ }
7904
+ const obj = asObject(current);
7905
+ if (!obj) {
7906
+ continue;
7907
+ }
7908
+ for (const value of Object.values(obj)) {
7909
+ queue.push(value);
7910
+ }
7911
+ }
7912
+ return Array.from(tokens);
7913
+ }
7677
7914
  function buildFieldIndex(form) {
7678
7915
  const byId = new Map();
7679
7916
  const byTitle = new Map();
@@ -8414,6 +8651,48 @@ function buildErrorExampleCalls(params) {
8414
8651
  }
8415
8652
  ];
8416
8653
  }
8654
+ if (errorCode === "FIELD_VALUE_FORMAT_ERROR") {
8655
+ const expectedFormat = asObject(params.details?.expected_format);
8656
+ const kind = asNullableString(expectedFormat?.kind);
8657
+ if (kind === "member_list") {
8658
+ return [
8659
+ {
8660
+ tool: "qf_form_get",
8661
+ arguments: {
8662
+ app_key: appKey
8663
+ },
8664
+ note: "先查看字段 write_format,确认成员字段写入 shape"
8665
+ },
8666
+ {
8667
+ tool: "qf_department_users_list",
8668
+ arguments: {
8669
+ dept_id: 111,
8670
+ fetch_child: false
8671
+ },
8672
+ note: "查询有效成员 userId,再按 [{userId,userName}] 写入"
8673
+ }
8674
+ ];
8675
+ }
8676
+ if (kind === "department_list") {
8677
+ return [
8678
+ {
8679
+ tool: "qf_form_get",
8680
+ arguments: {
8681
+ app_key: appKey
8682
+ },
8683
+ note: "先查看字段 write_format,确认部门字段写入 shape"
8684
+ },
8685
+ {
8686
+ tool: "qf_departments_list",
8687
+ arguments: {
8688
+ keyword: "销售",
8689
+ limit: 20
8690
+ },
8691
+ note: "查询有效部门 deptId,再按 [{deptId,deptName}] 写入"
8692
+ }
8693
+ ];
8694
+ }
8695
+ }
8417
8696
  if (errorCode === "INTERNAL_ERROR" || errorCode === "UNKNOWN_ERROR") {
8418
8697
  return [
8419
8698
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qingflow-mcp",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",