qingflow-mcp 0.3.15 → 0.3.16

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 (2) hide show
  1. package/dist/server.js +598 -106
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -63,7 +63,9 @@ const ADAPTIVE_TARGET_PAGE_MS = toPositiveInt(process.env.QINGFLOW_ADAPTIVE_TARG
63
63
  const MAX_LIST_ITEMS_BYTES = toPositiveInt(process.env.QINGFLOW_LIST_MAX_ITEMS_BYTES) ?? 400000;
64
64
  const REQUEST_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_REQUEST_TIMEOUT_MS) ?? 18000;
65
65
  const EXECUTION_BUDGET_MS = toPositiveInt(process.env.QINGFLOW_EXECUTION_BUDGET_MS) ?? 20000;
66
- const SERVER_VERSION = "0.3.14";
66
+ const WAIT_RESULT_DEFAULT_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_WAIT_RESULT_TIMEOUT_MS) ?? 5000;
67
+ const WAIT_RESULT_POLL_INTERVAL_MS = toPositiveInt(process.env.QINGFLOW_WAIT_RESULT_POLL_INTERVAL_MS) ?? 500;
68
+ const SERVER_VERSION = "0.3.16";
67
69
  const accessToken = process.env.QINGFLOW_ACCESS_TOKEN;
68
70
  const baseUrl = process.env.QINGFLOW_BASE_URL;
69
71
  if (!accessToken) {
@@ -174,6 +176,7 @@ const queryContractFields = {
174
176
  output_profile: outputProfileSchema.optional(),
175
177
  completeness: completenessSchema.optional(),
176
178
  evidence: z.record(z.unknown()).optional(),
179
+ resolved_mappings: z.record(z.unknown()).optional(),
177
180
  error_code: z.null().optional(),
178
181
  fix_hint: z.null().optional(),
179
182
  next_page_token: z.string().nullable().optional()
@@ -484,6 +487,8 @@ const createInputPublicSchema = z
484
487
  app_key: publicStringSchema,
485
488
  user_id: publicStringSchema.optional(),
486
489
  force_refresh_form: z.boolean().optional(),
490
+ wait_result: z.boolean().optional(),
491
+ wait_timeout_ms: z.number().int().positive().max(20000).optional(),
487
492
  apply_user: publicApplyUserSchema.optional(),
488
493
  answers: z.array(publicAnswerInputSchema).optional(),
489
494
  fields: z.record(z.unknown()).optional()
@@ -493,6 +498,8 @@ const createInputSchema = z
493
498
  app_key: z.string().min(1),
494
499
  user_id: z.string().min(1).optional(),
495
500
  force_refresh_form: z.boolean().optional(),
501
+ wait_result: z.boolean().optional(),
502
+ wait_timeout_ms: z.number().int().positive().max(20000).optional(),
496
503
  apply_user: z
497
504
  .object({
498
505
  email: z.string().optional(),
@@ -512,6 +519,9 @@ const createSuccessOutputSchema = z.object({
512
519
  data: z.object({
513
520
  request_id: z.string().nullable(),
514
521
  apply_id: z.union([z.string(), z.number(), z.null()]),
522
+ resolved: z.boolean().optional(),
523
+ timed_out: z.boolean().optional(),
524
+ operation_result: z.unknown().optional(),
515
525
  async_hint: z.string()
516
526
  }),
517
527
  meta: apiMetaSchema
@@ -523,6 +533,8 @@ const updateInputPublicSchema = z
523
533
  app_key: publicStringSchema.optional(),
524
534
  user_id: publicStringSchema.optional(),
525
535
  force_refresh_form: z.boolean().optional(),
536
+ wait_result: z.boolean().optional(),
537
+ wait_timeout_ms: z.number().int().positive().max(20000).optional(),
526
538
  answers: z.array(publicAnswerInputSchema).optional(),
527
539
  fields: z.record(z.unknown()).optional()
528
540
  });
@@ -532,6 +544,8 @@ const updateInputSchema = z
532
544
  app_key: z.string().min(1).optional(),
533
545
  user_id: z.string().min(1).optional(),
534
546
  force_refresh_form: z.boolean().optional(),
547
+ wait_result: z.boolean().optional(),
548
+ wait_timeout_ms: z.number().int().positive().max(20000).optional(),
535
549
  answers: z.array(answerInputSchema).optional(),
536
550
  fields: z.record(fieldValueSchema).optional()
537
551
  })
@@ -542,6 +556,10 @@ const updateSuccessOutputSchema = z.object({
542
556
  ok: z.literal(true),
543
557
  data: z.object({
544
558
  request_id: z.string().nullable(),
559
+ apply_id: z.union([z.string(), z.number(), z.null()]).optional(),
560
+ resolved: z.boolean().optional(),
561
+ timed_out: z.boolean().optional(),
562
+ operation_result: z.unknown().optional(),
545
563
  async_hint: z.string()
546
564
  }),
547
565
  meta: apiMetaSchema
@@ -592,7 +610,7 @@ const queryInputPublicSchema = z
592
610
  max_rows: z.number().int().positive().max(200).optional(),
593
611
  max_items: z.number().int().positive().max(200).optional(),
594
612
  max_columns: z.number().int().positive().max(MAX_COLUMN_LIMIT).optional(),
595
- select_columns: z.array(publicFieldSelectorSchema).min(1).max(MAX_COLUMN_LIMIT),
613
+ select_columns: z.array(publicFieldSelectorSchema).min(1).max(MAX_COLUMN_LIMIT).optional(),
596
614
  include_answers: z.boolean().optional(),
597
615
  amount_column: publicFieldSelectorSchema.optional(),
598
616
  time_range: publicTimeRangeSchema.optional(),
@@ -694,6 +712,7 @@ const querySummaryOutputSchema = z.object({
694
712
  rows: z.array(z.record(z.unknown())),
695
713
  completeness: completenessSchema.optional(),
696
714
  evidence: evidenceSchema.optional(),
715
+ resolved_mappings: z.record(z.unknown()).optional(),
697
716
  meta: z.object({
698
717
  field_mapping: z.array(z.object({
699
718
  role: z.enum(["row", "amount", "time"]),
@@ -773,7 +792,7 @@ const aggregateInputPublicSchema = z
773
792
  sort: z.array(publicSortItemSchema).optional(),
774
793
  filters: z.array(publicFilterItemSchema).optional(),
775
794
  time_range: publicTimeRangeSchema.optional(),
776
- group_by: z.array(publicFieldSelectorSchema).min(1).max(20),
795
+ group_by: z.array(publicFieldSelectorSchema).max(20).optional(),
777
796
  amount_column: publicFieldSelectorSchema.optional(),
778
797
  amount_columns: z.array(publicFieldSelectorSchema).min(1).max(5).optional(),
779
798
  metrics: z.array(z.enum(["count", "sum", "avg", "min", "max"])).min(1).max(5).optional(),
@@ -838,7 +857,7 @@ const aggregateInputSchema = z
838
857
  timezone: z.string().optional()
839
858
  })
840
859
  .optional(),
841
- group_by: z.array(z.union([z.string().min(1), z.number().int()])).min(1).max(20),
860
+ group_by: z.array(z.union([z.string().min(1), z.number().int()])).max(20).optional(),
842
861
  amount_column: z.union([z.string().min(1), z.number().int()]).optional(),
843
862
  amount_columns: z
844
863
  .array(z.union([z.string().min(1), z.number().int()]))
@@ -1308,7 +1327,7 @@ server.registerTool("qf_field_resolve", {
1308
1327
  });
1309
1328
  server.registerTool("qf_query_plan", {
1310
1329
  title: "Qingflow Query Plan",
1311
- description: "Preflight query arguments: normalize inputs, validate required fields, resolve mappings and estimate scan limits before execution.",
1330
+ description: "Debug/explain tool: preflight query arguments, validate required fields, resolve field mappings and estimate scan pages before actual execution. Use for complex queries or when troubleshooting. For normal queries, use qf_query directly.",
1312
1331
  inputSchema: queryPlanInputPublicSchema,
1313
1332
  outputSchema: queryPlanOutputSchema,
1314
1333
  annotations: {
@@ -1422,7 +1441,7 @@ server.registerTool("qf_export_json", {
1422
1441
  });
1423
1442
  server.registerTool("qf_query", {
1424
1443
  title: "Qingflow Unified Query",
1425
- description: "Unified read entry for list/record/summary. Use query_mode=auto to route automatically.",
1444
+ description: "Unified read entry for list/record/summary modes. Use query_mode=auto to route: apply_id→record; amount_column/time_range/stat_policy→summary; otherwise→list. In summary mode, select_columns is optional (auto-derived from amount_column/time_range). Field titles are accepted everywhere—que_id resolution is automatic.",
1426
1445
  inputSchema: queryInputPublicSchema,
1427
1446
  outputSchema: queryOutputSchema,
1428
1447
  annotations: {
@@ -1474,6 +1493,7 @@ server.registerTool("qf_query", {
1474
1493
  summary: executed.data
1475
1494
  },
1476
1495
  output_profile: executed.outputProfile,
1496
+ resolved_mappings: executed.resolvedMappings,
1477
1497
  ...(isVerboseProfile(executed.outputProfile)
1478
1498
  ? {
1479
1499
  completeness,
@@ -1502,6 +1522,7 @@ server.registerTool("qf_query", {
1502
1522
  list: executed.payload.data
1503
1523
  },
1504
1524
  output_profile: executed.outputProfile,
1525
+ resolved_mappings: executed.resolvedMappings,
1505
1526
  ...(isVerboseProfile(executed.outputProfile)
1506
1527
  ? {
1507
1528
  completeness,
@@ -1524,7 +1545,7 @@ server.registerTool("qf_query", {
1524
1545
  });
1525
1546
  server.registerTool("qf_record_create", {
1526
1547
  title: "Qingflow Record Create",
1527
- description: "Create one record. Supports explicit answers and ergonomic fields mapping (title or queId).",
1548
+ description: "Create one record. Supports fields{} mapping by title or queId, and explicit answers[]. Set wait_result=true to poll until the record is resolved (returns apply_id directly instead of requiring a follow-up qf_operation_get call).",
1528
1549
  inputSchema: createInputPublicSchema,
1529
1550
  outputSchema: createOutputSchema,
1530
1551
  annotations: {
@@ -1540,7 +1561,8 @@ server.registerTool("qf_record_create", {
1540
1561
  const normalizedAnswers = resolveAnswers({
1541
1562
  explicitAnswers: parsedArgs.answers,
1542
1563
  fields: parsedArgs.fields,
1543
- form: form?.result
1564
+ form: form?.result,
1565
+ tool: "qf_record_create"
1544
1566
  });
1545
1567
  const payload = {
1546
1568
  answers: normalizedAnswers
@@ -1552,12 +1574,34 @@ server.registerTool("qf_record_create", {
1552
1574
  userId: parsedArgs.user_id
1553
1575
  });
1554
1576
  const result = asObject(response.result);
1577
+ const requestId = asNullableString(result?.requestId);
1578
+ const immediateApplyId = result?.applyId ?? null;
1579
+ const shouldWaitForResult = (parsedArgs.wait_result ?? false) && requestId !== null && immediateApplyId === null;
1580
+ let finalApplyId = immediateApplyId;
1581
+ let isResolved = immediateApplyId !== null;
1582
+ let isTimedOut = false;
1583
+ let operationResult = null;
1584
+ if (shouldWaitForResult) {
1585
+ const waited = await waitForOperationResolution({
1586
+ requestId: requestId,
1587
+ timeoutMs: parsedArgs.wait_timeout_ms ?? WAIT_RESULT_DEFAULT_TIMEOUT_MS
1588
+ });
1589
+ isResolved = waited.resolved;
1590
+ isTimedOut = waited.timedOut;
1591
+ operationResult = waited.operationResult;
1592
+ finalApplyId = waited.applyId;
1593
+ }
1555
1594
  return okResult({
1556
1595
  ok: true,
1557
1596
  data: {
1558
- request_id: asNullableString(result?.requestId),
1559
- apply_id: result?.applyId ?? null,
1560
- async_hint: "Use qf_operation_get with request_id when apply_id is null."
1597
+ request_id: requestId,
1598
+ apply_id: finalApplyId,
1599
+ resolved: isResolved,
1600
+ ...(isTimedOut ? { timed_out: true } : {}),
1601
+ ...(operationResult !== null ? { operation_result: operationResult } : {}),
1602
+ async_hint: isResolved
1603
+ ? "Record created and resolved."
1604
+ : "Use qf_operation_get with request_id to fetch the result."
1561
1605
  },
1562
1606
  meta: buildMeta(response)
1563
1607
  }, `Create request sent for app ${parsedArgs.app_key}`);
@@ -1568,7 +1612,7 @@ server.registerTool("qf_record_create", {
1568
1612
  });
1569
1613
  server.registerTool("qf_record_update", {
1570
1614
  title: "Qingflow Record Update",
1571
- description: "Patch one record by applyId with explicit answers or ergonomic fields mapping.",
1615
+ description: "Patch one record by applyId with explicit answers or ergonomic fields mapping (title or queId). Set wait_result=true to poll until the update is confirmed instead of requiring a follow-up qf_operation_get call.",
1572
1616
  inputSchema: updateInputPublicSchema,
1573
1617
  outputSchema: updateOutputSchema,
1574
1618
  annotations: {
@@ -1588,15 +1632,38 @@ server.registerTool("qf_record_update", {
1588
1632
  const normalizedAnswers = resolveAnswers({
1589
1633
  explicitAnswers: parsedArgs.answers,
1590
1634
  fields: parsedArgs.fields,
1591
- form: form?.result
1635
+ form: form?.result,
1636
+ tool: "qf_record_update"
1592
1637
  });
1593
1638
  const response = await client.updateRecord(String(parsedArgs.apply_id), { answers: normalizedAnswers }, { userId: parsedArgs.user_id });
1594
1639
  const result = asObject(response.result);
1640
+ const updateRequestId = asNullableString(result?.requestId);
1641
+ const shouldWaitForUpdate = (parsedArgs.wait_result ?? false) && updateRequestId !== null;
1642
+ let updateIsResolved = false;
1643
+ let updateIsTimedOut = false;
1644
+ let updateOperationResult = null;
1645
+ let updateApplyId = null;
1646
+ if (shouldWaitForUpdate) {
1647
+ const waited = await waitForOperationResolution({
1648
+ requestId: updateRequestId,
1649
+ timeoutMs: parsedArgs.wait_timeout_ms ?? WAIT_RESULT_DEFAULT_TIMEOUT_MS
1650
+ });
1651
+ updateIsResolved = waited.resolved;
1652
+ updateIsTimedOut = waited.timedOut;
1653
+ updateOperationResult = waited.operationResult;
1654
+ updateApplyId = waited.applyId;
1655
+ }
1595
1656
  return okResult({
1596
1657
  ok: true,
1597
1658
  data: {
1598
- request_id: asNullableString(result?.requestId),
1599
- async_hint: "Use qf_operation_get with request_id to fetch update result when needed."
1659
+ request_id: updateRequestId,
1660
+ ...(updateApplyId !== null ? { apply_id: updateApplyId } : {}),
1661
+ resolved: updateIsResolved,
1662
+ ...(updateIsTimedOut ? { timed_out: true } : {}),
1663
+ ...(updateOperationResult !== null ? { operation_result: updateOperationResult } : {}),
1664
+ async_hint: updateIsResolved
1665
+ ? "Record updated and resolved."
1666
+ : "Use qf_operation_get with request_id to fetch the update result."
1600
1667
  },
1601
1668
  meta: buildMeta(response)
1602
1669
  }, `Update request sent for apply ${String(parsedArgs.apply_id)}`);
@@ -1632,7 +1699,7 @@ server.registerTool("qf_operation_get", {
1632
1699
  });
1633
1700
  server.registerTool("qf_records_aggregate", {
1634
1701
  title: "Qingflow Records Aggregate",
1635
- description: "Aggregate records by group_by columns with optional amount metrics. Designed for deterministic, auditable statistics.",
1702
+ description: "Aggregate records with optional group_by columns and amount metrics. Omit group_by for total-only summary (count/sum/avg across all records). Field titles are resolved automatically. Designed for deterministic, auditable statistics.",
1636
1703
  inputSchema: aggregateInputPublicSchema,
1637
1704
  outputSchema: aggregateOutputSchema,
1638
1705
  annotations: {
@@ -2068,7 +2135,8 @@ function buildToolSpecCatalog() {
2068
2135
  required: ["tool"],
2069
2136
  limits: {
2070
2137
  tool: "qf_records_list|qf_record_get|qf_query|qf_records_aggregate|qf_records_batch_get|qf_export_csv|qf_export_json",
2071
- input_contract: "strict JSON only; arguments must be a native JSON object"
2138
+ input_contract: "strict JSON only; arguments must be a native JSON object",
2139
+ usage_hint: "Debug/explain only. For normal queries use qf_query directly."
2072
2140
  },
2073
2141
  aliases: {},
2074
2142
  minimal_example: {
@@ -2185,9 +2253,9 @@ function buildToolSpecCatalog() {
2185
2253
  {
2186
2254
  tool: "qf_query",
2187
2255
  required: [
2188
- "record mode: apply_id + select_columns",
2256
+ "record mode: apply_id (select_columns recommended)",
2189
2257
  "list mode: app_key + select_columns",
2190
- "summary mode: app_key + select_columns"
2258
+ "summary mode: app_key only (select_columns auto-derived from amount_column/time_range)"
2191
2259
  ],
2192
2260
  limits: {
2193
2261
  query_mode: "auto|list|record|summary",
@@ -2219,7 +2287,7 @@ function buildToolSpecCatalog() {
2219
2287
  },
2220
2288
  {
2221
2289
  tool: "qf_records_aggregate",
2222
- required: ["app_key", "group_by"],
2290
+ required: ["app_key"],
2223
2291
  limits: {
2224
2292
  page_size_max: 200,
2225
2293
  requested_pages_max: 500,
@@ -2255,6 +2323,8 @@ function buildToolSpecCatalog() {
2255
2323
  required: ["app_key", "answers or fields"],
2256
2324
  limits: {
2257
2325
  write_mode: "Provide either answers[] or fields{}",
2326
+ wait_result: "optional boolean; when true, polls qf_operation_get internally and returns resolved apply_id directly",
2327
+ wait_timeout_ms: "optional int (max 20000); default 5000ms",
2258
2328
  input_contract: "strict JSON only; answers must be array and fields must be object"
2259
2329
  },
2260
2330
  aliases: {},
@@ -2271,6 +2341,8 @@ function buildToolSpecCatalog() {
2271
2341
  required: ["apply_id", "answers or fields"],
2272
2342
  limits: {
2273
2343
  write_mode: "Provide either answers[] or fields{}",
2344
+ wait_result: "optional boolean; when true, polls qf_operation_get internally and returns resolved result directly",
2345
+ wait_timeout_ms: "optional int (max 20000); default 5000ms",
2274
2346
  input_contract: "strict JSON only; answers must be array and fields must be object"
2275
2347
  },
2276
2348
  aliases: {},
@@ -3590,6 +3662,149 @@ function scoreFieldMatches(requested, fields, fuzzy, topK) {
3590
3662
  }
3591
3663
  return scored.sort((a, b) => b.score - a.score).slice(0, topK);
3592
3664
  }
3665
+ function buildFieldCandidateList(fields) {
3666
+ return fields
3667
+ .filter((field) => field.queId !== undefined && field.queId !== null)
3668
+ .map((field) => ({
3669
+ que_id: normalizeQueId(field.queId),
3670
+ que_title: asNullableString(field.queTitle),
3671
+ que_type: field.queType
3672
+ }));
3673
+ }
3674
+ function buildFieldSuggestions(requested, index, topK = 3) {
3675
+ return scoreFieldMatches(requested, Array.from(index.byId.values()), true, topK);
3676
+ }
3677
+ function buildResolvedMappingEntry(params) {
3678
+ const field = params.field ?? null;
3679
+ return {
3680
+ requested: params.requested,
3681
+ resolved: params.resolved,
3682
+ que_id: field?.queId !== undefined && field?.queId !== null ? normalizeQueId(field.queId) : null,
3683
+ que_title: asNullableString(field?.queTitle),
3684
+ que_type: field?.queType ?? null,
3685
+ ...(params.reason ? { reason: params.reason } : {}),
3686
+ ...(params.auto_selected ? { auto_selected: true } : {})
3687
+ };
3688
+ }
3689
+ function resolveFieldSelectorStrict(params) {
3690
+ const requested = String(params.fieldKey ?? "").trim();
3691
+ if (!requested) {
3692
+ throw new InputValidationError({
3693
+ message: `${params.location} contains an empty field selector`,
3694
+ errorCode: "EMPTY_FIELD_SELECTOR",
3695
+ fixHint: "Pass a non-empty field title or que_id.",
3696
+ details: {
3697
+ tool: params.tool,
3698
+ location: params.location
3699
+ }
3700
+ });
3701
+ }
3702
+ let resolved = null;
3703
+ if (isNumericKey(requested)) {
3704
+ resolved = params.index.byId.get(String(Number(requested))) ?? null;
3705
+ if (!resolved) {
3706
+ throw new InputValidationError({
3707
+ message: `${params.location} references unknown que_id "${requested}"`,
3708
+ errorCode: "FIELD_NOT_FOUND",
3709
+ fixHint: "Use qf_form_get or qf_field_resolve to confirm the exact field que_id before retrying.",
3710
+ details: {
3711
+ tool: params.tool,
3712
+ location: params.location,
3713
+ requested,
3714
+ suggestions: buildFieldSuggestions(requested, params.index)
3715
+ }
3716
+ });
3717
+ }
3718
+ }
3719
+ else {
3720
+ const matches = params.index.byTitle.get(requested.toLowerCase()) ?? [];
3721
+ if (matches.length === 1) {
3722
+ resolved = matches[0];
3723
+ }
3724
+ else if (matches.length > 1) {
3725
+ throw new InputValidationError({
3726
+ message: `${params.location} field "${requested}" is ambiguous`,
3727
+ errorCode: "AMBIGUOUS_FIELD",
3728
+ fixHint: "Use numeric que_id, or call qf_field_resolve first to disambiguate the field title.",
3729
+ details: {
3730
+ tool: params.tool,
3731
+ location: params.location,
3732
+ requested,
3733
+ candidates: buildFieldCandidateList(matches)
3734
+ }
3735
+ });
3736
+ }
3737
+ else {
3738
+ throw new InputValidationError({
3739
+ message: `${params.location} cannot resolve field "${requested}"`,
3740
+ errorCode: "FIELD_NOT_FOUND",
3741
+ fixHint: "Use qf_form_get or qf_field_resolve to confirm the exact field title before retrying.",
3742
+ details: {
3743
+ tool: params.tool,
3744
+ location: params.location,
3745
+ requested,
3746
+ suggestions: buildFieldSuggestions(requested, params.index)
3747
+ }
3748
+ });
3749
+ }
3750
+ }
3751
+ if (params.expectDateType && !isDateLikeQueType(resolved.queType)) {
3752
+ throw new InputValidationError({
3753
+ message: `Field "${requested}" is not a date field and cannot be used in ${params.location}`,
3754
+ errorCode: "TIME_RANGE_FIELD_TYPE_MISMATCH",
3755
+ fixHint: "Use qf_form_get to pick a date field (queType=4), then retry time_range with that field.",
3756
+ details: {
3757
+ tool: params.tool,
3758
+ location: params.location,
3759
+ requested,
3760
+ resolved_field: buildResolvedMappingEntry({
3761
+ requested,
3762
+ field: resolved,
3763
+ resolved: true
3764
+ })
3765
+ }
3766
+ });
3767
+ }
3768
+ return resolved;
3769
+ }
3770
+ function resolveOutputColumn(column, index, label, tool) {
3771
+ const requested = String(column).trim();
3772
+ if (!requested) {
3773
+ throw new InputValidationError({
3774
+ message: `${label} contains an empty column selector`,
3775
+ errorCode: "EMPTY_FIELD_SELECTOR",
3776
+ fixHint: "Pass a non-empty field title or que_id.",
3777
+ details: {
3778
+ tool,
3779
+ location: label
3780
+ }
3781
+ });
3782
+ }
3783
+ if (isNumericKey(requested)) {
3784
+ const hit = index.byId.get(String(Number(requested)));
3785
+ return {
3786
+ requested,
3787
+ que_id: hit?.queId !== undefined && hit?.queId !== null ? normalizeQueId(hit.queId) : Number(requested),
3788
+ que_title: asNullableString(hit?.queTitle),
3789
+ que_type: hit?.queType ?? null
3790
+ };
3791
+ }
3792
+ const hit = resolveFieldSelectorStrict({
3793
+ fieldKey: requested,
3794
+ index,
3795
+ tool,
3796
+ location: label
3797
+ });
3798
+ return {
3799
+ requested,
3800
+ que_id: normalizeQueId(hit.queId),
3801
+ que_title: asNullableString(hit.queTitle),
3802
+ que_type: hit.queType
3803
+ };
3804
+ }
3805
+ function resolveOutputColumns(columns, index, label, tool) {
3806
+ return normalizeColumnSelectors(columns).map((requested) => resolveOutputColumn(requested, index, label, tool));
3807
+ }
3593
3808
  function normalizedTextSimilarity(left, right) {
3594
3809
  if (!left || !right) {
3595
3810
  return 0;
@@ -4139,6 +4354,18 @@ async function executeRecordsExport(format, args) {
4139
4354
  });
4140
4355
  }
4141
4356
  const outputProfile = resolveOutputProfile(args.output_profile);
4357
+ const form = await getFormCached(args.app_key, args.user_id, false);
4358
+ const index = buildFieldIndex(form.result);
4359
+ const selectResolution = resolveSelectColumnsWithIndex(args.select_columns, index, `qf_export_${format}`);
4360
+ const filterResolution = resolveFiltersWithIndex(args.filters, index, `qf_export_${format}`);
4361
+ const timeRangeResolution = resolveTimeRangeWithIndex(args.time_range, index, `qf_export_${format}`);
4362
+ const sortResolution = resolveSortWithIndex(args.sort, index, `qf_export_${format}`);
4363
+ const resolvedMappings = {
4364
+ select_columns: selectResolution.mappings,
4365
+ ...(filterResolution.mappings.length > 0 ? { filters: filterResolution.mappings } : {}),
4366
+ ...(sortResolution.mappings.length > 0 ? { sort: sortResolution.mappings } : {}),
4367
+ ...(timeRangeResolution.mapping ? { time_range: timeRangeResolution.mapping } : {})
4368
+ };
4142
4369
  const queryId = randomUUID();
4143
4370
  const pageNum = resolveStartPage(args.page_num, args.page_token, args.app_key);
4144
4371
  const requestedPages = args.requested_pages ?? EXPORT_DEFAULT_PAGES;
@@ -4146,14 +4373,12 @@ async function executeRecordsExport(format, args) {
4146
4373
  const maxRows = Math.min(args.max_rows ?? EXPORT_MAX_ROWS, EXPORT_MAX_ROWS);
4147
4374
  const startedAt = Date.now();
4148
4375
  const adaptivePaging = createAdaptivePagingState(args.page_size ?? DEFAULT_PAGE_SIZE);
4149
- const effectiveFilters = appendTimeRangeFilter(args.filters, args.time_range);
4150
- assertTimeRangeFilterApplied(`qf_export_${format}`, args.time_range, effectiveFilters);
4376
+ const effectiveFilters = appendTimeRangeFilter(filterResolution.filters, timeRangeResolution.time_range);
4377
+ assertTimeRangeFilterApplied(`qf_export_${format}`, timeRangeResolution.time_range, effectiveFilters);
4151
4378
  if (hasDateLikeRangeFilters(effectiveFilters)) {
4152
- const form = await getFormCached(args.app_key, args.user_id, false);
4153
- const index = buildFieldIndex(form.result);
4154
4379
  validateDateRangeFilters(effectiveFilters, index, `qf_export_${format}`);
4155
4380
  }
4156
- const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
4381
+ const normalizedSort = sortResolution.sort;
4157
4382
  let currentPage = pageNum;
4158
4383
  let fetchedPages = 0;
4159
4384
  let hasMore = false;
@@ -4220,27 +4445,29 @@ async function executeRecordsExport(format, args) {
4220
4445
  throw new Error(`Failed to export ${format}: empty response`);
4221
4446
  }
4222
4447
  const normalizedItems = rawItems.map((raw) => normalizeRecordItem(raw, true));
4448
+ const requestedSelectColumns = selectResolution.columns.map((item) => item.requested);
4223
4449
  const projection = projectRecordItemsColumns({
4224
4450
  items: normalizedItems,
4225
4451
  includeAnswers: true,
4226
4452
  maxColumns: args.max_columns,
4227
- selectColumns: args.select_columns
4453
+ selectColumns: requestedSelectColumns
4228
4454
  });
4229
4455
  if (normalizedItems.length > 0 && projection.matchedAnswersCount === 0) {
4230
4456
  throw new InputValidationError({
4231
- message: `No answers matched select_columns (${args.select_columns
4457
+ message: `No answers matched select_columns (${requestedSelectColumns
4232
4458
  .map((item) => String(item))
4233
4459
  .join(", ")}).`,
4234
4460
  errorCode: "COLUMN_SELECTOR_NOT_FOUND",
4235
- fixHint: "Use qf_form_get to confirm que_id/que_title. If parameters were stringified, pass native JSON arrays (or plain arrays) for select_columns.",
4461
+ fixHint: "Use qf_form_get or qf_field_resolve to confirm the exact field title/que_id before retrying.",
4236
4462
  details: {
4237
- select_columns: args.select_columns
4463
+ select_columns: requestedSelectColumns,
4464
+ resolved_mappings: selectResolution.mappings
4238
4465
  }
4239
4466
  });
4240
4467
  }
4241
4468
  const selectedColumnsForRows = args.max_columns !== undefined
4242
- ? projection.selectedColumns.slice(0, args.max_columns)
4243
- : projection.selectedColumns;
4469
+ ? requestedSelectColumns.slice(0, args.max_columns)
4470
+ : requestedSelectColumns;
4244
4471
  const rows = buildFlatRowsFromItems({
4245
4472
  items: normalizedItems,
4246
4473
  selectedColumns: selectedColumnsForRows
@@ -4320,6 +4547,7 @@ async function executeRecordsExport(format, args) {
4320
4547
  ? {
4321
4548
  completeness,
4322
4549
  evidence,
4550
+ resolved_mappings: resolvedMappings,
4323
4551
  execution: {
4324
4552
  scanned_pages: fetchedPages,
4325
4553
  requested_pages: requestedPages,
@@ -4347,7 +4575,8 @@ async function executeRecordsExport(format, args) {
4347
4575
  };
4348
4576
  return {
4349
4577
  payload,
4350
- message: `Exported ${rows.length} rows to ${filePath}`
4578
+ message: `Exported ${rows.length} rows to ${filePath}`,
4579
+ resolvedMappings
4351
4580
  };
4352
4581
  }
4353
4582
  async function executeRecordsList(args) {
@@ -4366,20 +4595,30 @@ async function executeRecordsList(args) {
4366
4595
  });
4367
4596
  }
4368
4597
  const outputProfile = resolveOutputProfile(args.output_profile);
4598
+ const form = await getFormCached(args.app_key, args.user_id, false);
4599
+ const index = buildFieldIndex(form.result);
4600
+ const selectResolution = resolveSelectColumnsWithIndex(args.select_columns, index, "qf_records_list");
4601
+ const filterResolution = resolveFiltersWithIndex(args.filters, index, "qf_records_list");
4602
+ const timeRangeResolution = resolveTimeRangeWithIndex(args.time_range, index, "qf_records_list");
4603
+ const sortResolution = resolveSortWithIndex(args.sort, index, "qf_records_list");
4604
+ const resolvedMappings = {
4605
+ select_columns: selectResolution.mappings,
4606
+ ...(filterResolution.mappings.length > 0 ? { filters: filterResolution.mappings } : {}),
4607
+ ...(sortResolution.mappings.length > 0 ? { sort: sortResolution.mappings } : {}),
4608
+ ...(timeRangeResolution.mapping ? { time_range: timeRangeResolution.mapping } : {})
4609
+ };
4369
4610
  const queryId = randomUUID();
4370
4611
  const pageNum = resolveStartPage(args.page_num, args.page_token, args.app_key);
4371
4612
  const pageSize = args.page_size ?? DEFAULT_PAGE_SIZE;
4372
4613
  const adaptivePaging = createAdaptivePagingState(pageSize);
4373
4614
  const requestedPages = args.requested_pages ?? 1;
4374
4615
  const scanMaxPages = args.scan_max_pages ?? requestedPages;
4375
- const effectiveFilters = appendTimeRangeFilter(args.filters, args.time_range);
4376
- assertTimeRangeFilterApplied("qf_records_list", args.time_range, effectiveFilters);
4616
+ const effectiveFilters = appendTimeRangeFilter(filterResolution.filters, timeRangeResolution.time_range);
4617
+ assertTimeRangeFilterApplied("qf_records_list", timeRangeResolution.time_range, effectiveFilters);
4377
4618
  if (hasDateLikeRangeFilters(effectiveFilters)) {
4378
- const form = await getFormCached(args.app_key, args.user_id, false);
4379
- const index = buildFieldIndex(form.result);
4380
4619
  validateDateRangeFilters(effectiveFilters, index, "qf_records_list");
4381
4620
  }
4382
- const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
4621
+ const normalizedSort = sortResolution.sort;
4383
4622
  const includeAnswers = true;
4384
4623
  const startedAt = Date.now();
4385
4624
  let currentPage = pageNum;
@@ -4451,27 +4690,29 @@ async function executeRecordsList(args) {
4451
4690
  .slice(0, listLimit.limit)
4452
4691
  .map((raw) => normalizeRecordItem(raw, includeAnswers));
4453
4692
  const sourceItemsForRows = items.slice();
4693
+ const requestedSelectColumns = selectResolution.columns.map((item) => item.requested);
4454
4694
  const columnProjection = projectRecordItemsColumns({
4455
4695
  items,
4456
4696
  includeAnswers,
4457
4697
  maxColumns: args.max_columns,
4458
- selectColumns: args.select_columns
4698
+ selectColumns: requestedSelectColumns
4459
4699
  });
4460
4700
  if (items.length > 0 && columnProjection.matchedAnswersCount === 0) {
4461
4701
  throw new InputValidationError({
4462
- message: `No answers matched select_columns (${args.select_columns
4702
+ message: `No answers matched select_columns (${requestedSelectColumns
4463
4703
  .map((item) => String(item))
4464
4704
  .join(", ")}).`,
4465
4705
  errorCode: "COLUMN_SELECTOR_NOT_FOUND",
4466
- fixHint: "Use qf_form_get to confirm que_id/que_title. If parameters were stringified, pass native JSON arrays (or plain arrays) for select_columns.",
4706
+ fixHint: "Use qf_form_get or qf_field_resolve to confirm the exact field title/que_id before retrying.",
4467
4707
  details: {
4468
- select_columns: args.select_columns
4708
+ select_columns: requestedSelectColumns,
4709
+ resolved_mappings: selectResolution.mappings
4469
4710
  }
4470
4711
  });
4471
4712
  }
4472
4713
  const selectedColumnsForRows = args.max_columns !== undefined
4473
- ? columnProjection.selectedColumns.slice(0, args.max_columns)
4474
- : columnProjection.selectedColumns;
4714
+ ? requestedSelectColumns.slice(0, args.max_columns)
4715
+ : requestedSelectColumns;
4475
4716
  const rows = buildFlatRowsFromItems({
4476
4717
  items: sourceItemsForRows,
4477
4718
  selectedColumns: selectedColumnsForRows
@@ -4509,14 +4750,14 @@ async function executeRecordsList(args) {
4509
4750
  const listState = {
4510
4751
  query_id: queryId,
4511
4752
  app_key: args.app_key,
4512
- selected_columns: columnProjection.selectedColumns,
4753
+ selected_columns: requestedSelectColumns,
4513
4754
  filters: echoFilters(effectiveFilters),
4514
- time_range: args.time_range
4755
+ time_range: timeRangeResolution.mapping
4515
4756
  ? {
4516
- column: String(args.time_range.column),
4517
- from: args.time_range.from ?? null,
4518
- to: args.time_range.to ?? null,
4519
- timezone: args.time_range.timezone ?? null
4757
+ column: String(args.time_range?.column ?? timeRangeResolution.mapping.requested ?? ""),
4758
+ from: timeRangeResolution.time_range?.from ?? null,
4759
+ to: timeRangeResolution.time_range?.to ?? null,
4760
+ timezone: timeRangeResolution.time_range?.timezone ?? null
4520
4761
  }
4521
4762
  : null
4522
4763
  };
@@ -4553,6 +4794,7 @@ async function executeRecordsList(args) {
4553
4794
  : {})
4554
4795
  },
4555
4796
  output_profile: outputProfile,
4797
+ resolved_mappings: resolvedMappings,
4556
4798
  ...(isVerboseProfile(outputProfile)
4557
4799
  ? {
4558
4800
  completeness,
@@ -4577,7 +4819,8 @@ async function executeRecordsList(args) {
4577
4819
  }),
4578
4820
  completeness,
4579
4821
  evidence,
4580
- outputProfile
4822
+ outputProfile,
4823
+ resolvedMappings
4581
4824
  };
4582
4825
  }
4583
4826
  async function executeRecordGet(args) {
@@ -4772,13 +5015,6 @@ async function executeRecordsSummary(args) {
4772
5015
  fixHint: "Provide app_key, for example: {\"query_mode\":\"summary\",\"app_key\":\"21b3d559\",...}"
4773
5016
  });
4774
5017
  }
4775
- if (!args.select_columns?.length) {
4776
- throw missingRequiredFieldError({
4777
- field: "select_columns",
4778
- tool: "qf_query(summary)",
4779
- fixHint: "Provide select_columns as an array (<=2), for example: {\"select_columns\":[\"客户全称\"]}"
4780
- });
4781
- }
4782
5018
  const outputProfile = resolveOutputProfile(args.output_profile);
4783
5019
  const strictFull = args.strict_full ?? true;
4784
5020
  const includeNegative = args.stat_policy?.include_negative ?? true;
@@ -4793,7 +5029,10 @@ async function executeRecordsSummary(args) {
4793
5029
  const timezone = args.time_range?.timezone ?? "Asia/Shanghai";
4794
5030
  const form = await getFormCached(args.app_key, args.user_id, false);
4795
5031
  const index = buildFieldIndex(form.result);
4796
- const selectedColumns = resolveSummaryColumns(args.select_columns, index, "select_columns");
5032
+ const selectResolution = args.select_columns && args.select_columns.length > 0
5033
+ ? resolveSelectColumnsWithIndex(args.select_columns, index, "qf_query(summary)")
5034
+ : buildDefaultSummarySelectColumns(args, index);
5035
+ const selectedColumns = selectResolution.columns;
4797
5036
  const effectiveColumns = args.max_columns !== undefined ? selectedColumns.slice(0, args.max_columns) : selectedColumns;
4798
5037
  if (effectiveColumns.length === 0) {
4799
5038
  throw new Error("No output columns remain after max_columns cap");
@@ -4801,14 +5040,28 @@ async function executeRecordsSummary(args) {
4801
5040
  const amountColumn = args.amount_column !== undefined
4802
5041
  ? resolveSummaryColumn(args.amount_column, index, "amount_column")
4803
5042
  : null;
4804
- const timeColumn = args.time_range ? resolveSummaryColumn(args.time_range.column, index, "time_range.column") : null;
4805
- const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
4806
- const summaryFilters = [...(args.filters ?? [])];
4807
- if (timeColumn && (args.time_range?.from || args.time_range?.to)) {
5043
+ const timeRangeResolution = resolveTimeRangeWithIndex(args.time_range, index, "qf_query(summary)");
5044
+ const timeColumn = timeRangeResolution.time_range
5045
+ ? resolveSummaryColumn(timeRangeResolution.time_range.column, index, "time_range.column")
5046
+ : null;
5047
+ const filterResolution = resolveFiltersWithIndex(args.filters, index, "qf_query(summary)");
5048
+ const sortResolution = resolveSortWithIndex(args.sort, index, "qf_query(summary)");
5049
+ const resolvedMappings = {
5050
+ select_columns: selectResolution.mappings,
5051
+ ...(amountColumn
5052
+ ? { amount_column: buildResolvedMappingFromSummaryColumn(amountColumn) }
5053
+ : {}),
5054
+ ...(filterResolution.mappings.length > 0 ? { filters: filterResolution.mappings } : {}),
5055
+ ...(sortResolution.mappings.length > 0 ? { sort: sortResolution.mappings } : {}),
5056
+ ...(timeRangeResolution.mapping ? { time_range: timeRangeResolution.mapping } : {})
5057
+ };
5058
+ const normalizedSort = sortResolution.sort;
5059
+ const summaryFilters = [...(filterResolution.filters ?? [])];
5060
+ if (timeColumn && (timeRangeResolution.time_range?.from || timeRangeResolution.time_range?.to)) {
4808
5061
  summaryFilters.push({
4809
5062
  que_id: timeColumn.que_id,
4810
- ...(args.time_range.from ? { min_value: args.time_range.from } : {}),
4811
- ...(args.time_range.to ? { max_value: args.time_range.to } : {})
5063
+ ...(timeRangeResolution.time_range.from ? { min_value: timeRangeResolution.time_range.from } : {}),
5064
+ ...(timeRangeResolution.time_range.to ? { max_value: timeRangeResolution.time_range.to } : {})
4812
5065
  });
4813
5066
  }
4814
5067
  validateDateRangeFilters(summaryFilters, index, "qf_query(summary)");
@@ -4824,7 +5077,7 @@ async function executeRecordsSummary(args) {
4824
5077
  select_columns: effectiveColumns,
4825
5078
  amount_column: amountColumn,
4826
5079
  time_column: timeColumn,
4827
- time_range: args.time_range,
5080
+ time_range: timeRangeResolution.time_range,
4828
5081
  stat_policy: {
4829
5082
  include_negative: includeNegative,
4830
5083
  include_null: includeNull
@@ -4841,8 +5094,8 @@ async function executeRecordsSummary(args) {
4841
5094
  time_range: timeColumn
4842
5095
  ? {
4843
5096
  column: timeColumn.requested,
4844
- from: args.time_range?.from ?? null,
4845
- to: args.time_range?.to ?? null,
5097
+ from: timeRangeResolution.time_range?.from ?? null,
5098
+ to: timeRangeResolution.time_range?.to ?? null,
4846
5099
  timezone
4847
5100
  }
4848
5101
  : null
@@ -5061,6 +5314,7 @@ async function executeRecordsSummary(args) {
5061
5314
  },
5062
5315
  rows,
5063
5316
  completeness,
5317
+ resolved_mappings: resolvedMappings,
5064
5318
  ...(isVerboseProfile(outputProfile)
5065
5319
  ? {
5066
5320
  evidence,
@@ -5071,8 +5325,8 @@ async function executeRecordsSummary(args) {
5071
5325
  time_range: timeColumn
5072
5326
  ? {
5073
5327
  column: timeColumn.requested,
5074
- from: args.time_range?.from ?? null,
5075
- to: args.time_range?.to ?? null,
5328
+ from: timeRangeResolution.time_range?.from ?? null,
5329
+ to: timeRangeResolution.time_range?.to ?? null,
5076
5330
  timezone
5077
5331
  }
5078
5332
  : null
@@ -5099,7 +5353,8 @@ async function executeRecordsSummary(args) {
5099
5353
  : `Summarized ${scannedRecords}/${knownResultAmount} records (partial)`,
5100
5354
  completeness,
5101
5355
  evidence,
5102
- outputProfile
5356
+ outputProfile,
5357
+ resolvedMappings
5103
5358
  };
5104
5359
  }
5105
5360
  async function executeRecordsAggregate(args) {
@@ -5118,7 +5373,12 @@ async function executeRecordsAggregate(args) {
5118
5373
  const timeBucket = args.time_bucket ?? null;
5119
5374
  const form = await getFormCached(args.app_key, args.user_id, false);
5120
5375
  const index = buildFieldIndex(form.result);
5121
- const groupColumns = resolveSummaryColumns(args.group_by, index, "group_by");
5376
+ const filterResolution = resolveFiltersWithIndex(args.filters, index, "qf_records_aggregate");
5377
+ const sortResolution = resolveSortWithIndex(args.sort, index, "qf_records_aggregate");
5378
+ const timeRangeResolution = resolveTimeRangeWithIndex(args.time_range, index, "qf_records_aggregate");
5379
+ const groupColumns = args.group_by && args.group_by.length > 0
5380
+ ? resolveSummaryColumns(args.group_by, index, "group_by")
5381
+ : [];
5122
5382
  const amountSelectors = args.amount_columns && args.amount_columns.length > 0
5123
5383
  ? args.amount_columns
5124
5384
  : args.amount_column !== undefined
@@ -5129,14 +5389,25 @@ async function executeRecordsAggregate(args) {
5129
5389
  : [];
5130
5390
  const primaryAmountColumn = amountColumns[0] ?? null;
5131
5391
  const metrics = resolveAggregateMetrics(args.metrics, amountColumns.length > 0);
5132
- const timeColumn = args.time_range ? resolveSummaryColumn(args.time_range.column, index, "time_range.column") : null;
5133
- const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
5134
- const aggregateFilters = [...(args.filters ?? [])];
5135
- if (timeColumn && (args.time_range?.from || args.time_range?.to)) {
5392
+ const timeColumn = timeRangeResolution.time_range
5393
+ ? resolveSummaryColumn(timeRangeResolution.time_range.column, index, "time_range.column")
5394
+ : null;
5395
+ const resolvedMappings = {
5396
+ group_by: groupColumns.map((item) => buildResolvedMappingFromSummaryColumn(item)),
5397
+ ...(amountColumns.length > 0
5398
+ ? { amount_columns: amountColumns.map((item) => buildResolvedMappingFromSummaryColumn(item)) }
5399
+ : {}),
5400
+ ...(filterResolution.mappings.length > 0 ? { filters: filterResolution.mappings } : {}),
5401
+ ...(sortResolution.mappings.length > 0 ? { sort: sortResolution.mappings } : {}),
5402
+ ...(timeRangeResolution.mapping ? { time_range: timeRangeResolution.mapping } : {})
5403
+ };
5404
+ const normalizedSort = sortResolution.sort;
5405
+ const aggregateFilters = [...(filterResolution.filters ?? [])];
5406
+ if (timeColumn && (timeRangeResolution.time_range?.from || timeRangeResolution.time_range?.to)) {
5136
5407
  aggregateFilters.push({
5137
5408
  que_id: timeColumn.que_id,
5138
- ...(args.time_range.from ? { min_value: args.time_range.from } : {}),
5139
- ...(args.time_range.to ? { max_value: args.time_range.to } : {})
5409
+ ...(timeRangeResolution.time_range.from ? { min_value: timeRangeResolution.time_range.from } : {}),
5410
+ ...(timeRangeResolution.time_range.to ? { max_value: timeRangeResolution.time_range.to } : {})
5140
5411
  });
5141
5412
  }
5142
5413
  validateDateRangeFilters(aggregateFilters, index, "qf_records_aggregate");
@@ -5153,7 +5424,7 @@ async function executeRecordsAggregate(args) {
5153
5424
  amount_columns: amountColumns,
5154
5425
  metrics,
5155
5426
  time_column: timeColumn,
5156
- time_range: args.time_range,
5427
+ time_range: timeRangeResolution.time_range,
5157
5428
  time_bucket: timeBucket,
5158
5429
  stat_policy: {
5159
5430
  include_negative: includeNegative,
@@ -5174,8 +5445,8 @@ async function executeRecordsAggregate(args) {
5174
5445
  time_range: timeColumn
5175
5446
  ? {
5176
5447
  column: timeColumn.requested,
5177
- from: args.time_range?.from ?? null,
5178
- to: args.time_range?.to ?? null,
5448
+ from: timeRangeResolution.time_range?.from ?? null,
5449
+ to: timeRangeResolution.time_range?.to ?? null,
5179
5450
  timezone
5180
5451
  }
5181
5452
  : null
@@ -5441,6 +5712,7 @@ async function executeRecordsAggregate(args) {
5441
5712
  : {})
5442
5713
  },
5443
5714
  output_profile: outputProfile,
5715
+ resolved_mappings: resolvedMappings,
5444
5716
  ...(isVerboseProfile(outputProfile)
5445
5717
  ? {
5446
5718
  completeness,
@@ -5466,25 +5738,12 @@ function resolveSummaryColumns(columns, index, label) {
5466
5738
  }
5467
5739
  function resolveSummaryColumn(column, index, label) {
5468
5740
  const requested = String(column).trim();
5469
- if (!requested) {
5470
- throw new Error(`${label} contains an empty column selector`);
5471
- }
5472
- if (isNumericKey(requested)) {
5473
- const hit = index.byId.get(String(Number(requested)));
5474
- if (!hit) {
5475
- throw new Error(`${label} references unknown que_id "${requested}"`);
5476
- }
5477
- return {
5478
- requested,
5479
- que_id: normalizeQueId(hit.queId),
5480
- que_title: asNullableString(hit.queTitle),
5481
- que_type: hit.queType
5482
- };
5483
- }
5484
- const hit = resolveFieldByKey(requested, index);
5485
- if (!hit || hit.queId === undefined || hit.queId === null) {
5486
- throw new Error(`${label} cannot resolve field "${requested}"`);
5487
- }
5741
+ const hit = resolveFieldSelectorStrict({
5742
+ fieldKey: requested,
5743
+ index,
5744
+ tool: label.startsWith("group_by") ? "qf_records_aggregate" : "qf_query",
5745
+ location: label
5746
+ });
5488
5747
  return {
5489
5748
  requested,
5490
5749
  que_id: normalizeQueId(hit.queId),
@@ -5762,7 +6021,7 @@ function normalizeRecordItem(raw, includeAnswers) {
5762
6021
  return normalized;
5763
6022
  }
5764
6023
  function resolveAnswers(params) {
5765
- const normalizedFromFields = resolveFieldAnswers(params.fields, params.form);
6024
+ const normalizedFromFields = resolveFieldAnswers(params.fields, params.form, params.tool);
5766
6025
  const normalizedExplicit = normalizeExplicitAnswers(params.explicitAnswers);
5767
6026
  const merged = new Map();
5768
6027
  for (const answer of normalizedFromFields) {
@@ -5812,7 +6071,7 @@ function normalizeExplicitAnswers(answers) {
5812
6071
  }
5813
6072
  return output;
5814
6073
  }
5815
- function resolveFieldAnswers(fields, form) {
6074
+ function resolveFieldAnswers(fields, form, tool = "qf_record_create") {
5816
6075
  const entries = Object.entries(fields ?? {});
5817
6076
  if (entries.length === 0) {
5818
6077
  return [];
@@ -5820,9 +6079,30 @@ function resolveFieldAnswers(fields, form) {
5820
6079
  const index = buildFieldIndex(form);
5821
6080
  const answers = [];
5822
6081
  for (const [fieldKey, fieldValue] of entries) {
5823
- const field = resolveFieldByKey(fieldKey, index);
6082
+ let field;
6083
+ if (isNumericKey(fieldKey)) {
6084
+ field = resolveFieldByKey(fieldKey, index);
6085
+ }
6086
+ else {
6087
+ field = resolveFieldSelectorStrict({
6088
+ fieldKey,
6089
+ index,
6090
+ tool,
6091
+ location: `fields.${fieldKey}`
6092
+ });
6093
+ }
5824
6094
  if (!field) {
5825
- throw new Error(`Cannot resolve field key "${fieldKey}" from form metadata`);
6095
+ throw new InputValidationError({
6096
+ message: `Cannot resolve field key "${fieldKey}" from form metadata`,
6097
+ errorCode: "FIELD_NOT_FOUND",
6098
+ fixHint: "Use qf_form_get or qf_field_resolve to confirm the exact field title before retrying.",
6099
+ details: {
6100
+ tool,
6101
+ location: `fields.${fieldKey}`,
6102
+ requested: fieldKey,
6103
+ suggestions: buildFieldSuggestions(fieldKey, index)
6104
+ }
6105
+ });
5826
6106
  }
5827
6107
  answers.push(makeAnswerFromField(field, fieldValue));
5828
6108
  }
@@ -5971,15 +6251,227 @@ async function normalizeListSort(sort, appKey, userId) {
5971
6251
  const index = buildFieldIndex(form.result);
5972
6252
  return sort.map((item) => {
5973
6253
  const rawKey = String(item.que_id).trim();
5974
- const resolved = resolveFieldByKey(rawKey, index);
5975
- if (!resolved || resolved.queId === undefined || resolved.queId === null) {
5976
- throw new Error(`Cannot resolve sort.que_id "${rawKey}". Use numeric que_id or exact field title from qf_form_get.`);
6254
+ const resolved = resolveFieldSelectorStrict({
6255
+ fieldKey: rawKey,
6256
+ index,
6257
+ tool: "qf_records_list",
6258
+ location: "sort[].que_id"
6259
+ });
6260
+ return {
6261
+ que_id: normalizeQueId(resolved.queId),
6262
+ ...(item.ascend !== undefined ? { ascend: item.ascend } : {})
6263
+ };
6264
+ });
6265
+ }
6266
+ function buildResolvedMappingFromSummaryColumn(column, extra = {}) {
6267
+ return {
6268
+ requested: column.requested,
6269
+ resolved: true,
6270
+ que_id: column.que_id,
6271
+ que_title: column.que_title,
6272
+ que_type: column.que_type ?? null,
6273
+ ...extra
6274
+ };
6275
+ }
6276
+ function resolveFiltersWithIndex(filters, index, tool) {
6277
+ if (!filters?.length) {
6278
+ return {
6279
+ filters,
6280
+ mappings: []
6281
+ };
6282
+ }
6283
+ const mappings = [];
6284
+ const resolvedFilters = filters.map((filter, indexInArray) => {
6285
+ if (filter.que_id === undefined || filter.que_id === null) {
6286
+ return filter;
5977
6287
  }
6288
+ const resolved = resolveFieldSelectorStrict({
6289
+ fieldKey: filter.que_id,
6290
+ index,
6291
+ tool,
6292
+ location: `filters[${indexInArray}].que_id`
6293
+ });
6294
+ mappings.push(buildResolvedMappingEntry({
6295
+ requested: String(filter.que_id),
6296
+ field: resolved,
6297
+ resolved: true
6298
+ }));
6299
+ return {
6300
+ ...filter,
6301
+ que_id: normalizeQueId(resolved.queId)
6302
+ };
6303
+ });
6304
+ return {
6305
+ filters: resolvedFilters,
6306
+ mappings
6307
+ };
6308
+ }
6309
+ function resolveSortWithIndex(sort, index, tool) {
6310
+ if (!sort?.length) {
6311
+ return {
6312
+ sort,
6313
+ mappings: []
6314
+ };
6315
+ }
6316
+ const mappings = [];
6317
+ const resolvedSort = sort.map((item, indexInArray) => {
6318
+ const resolved = resolveFieldSelectorStrict({
6319
+ fieldKey: item.que_id,
6320
+ index,
6321
+ tool,
6322
+ location: `sort[${indexInArray}].que_id`
6323
+ });
6324
+ mappings.push(buildResolvedMappingEntry({
6325
+ requested: String(item.que_id),
6326
+ field: resolved,
6327
+ resolved: true
6328
+ }));
5978
6329
  return {
5979
6330
  que_id: normalizeQueId(resolved.queId),
5980
6331
  ...(item.ascend !== undefined ? { ascend: item.ascend } : {})
5981
6332
  };
5982
6333
  });
6334
+ return {
6335
+ sort: resolvedSort,
6336
+ mappings
6337
+ };
6338
+ }
6339
+ function resolveTimeRangeWithIndex(timeRange, index, tool) {
6340
+ if (!timeRange) {
6341
+ return {
6342
+ time_range: undefined,
6343
+ mapping: null
6344
+ };
6345
+ }
6346
+ const resolved = resolveFieldSelectorStrict({
6347
+ fieldKey: timeRange.column,
6348
+ index,
6349
+ tool,
6350
+ location: "time_range.column",
6351
+ expectDateType: true
6352
+ });
6353
+ return {
6354
+ time_range: {
6355
+ ...timeRange,
6356
+ column: normalizeQueId(resolved.queId)
6357
+ },
6358
+ mapping: buildResolvedMappingEntry({
6359
+ requested: String(timeRange.column),
6360
+ field: resolved,
6361
+ resolved: true
6362
+ })
6363
+ };
6364
+ }
6365
+ function resolveSelectColumnsWithIndex(selectColumns, index, tool) {
6366
+ const columns = resolveOutputColumns(selectColumns, index, "select_columns", tool);
6367
+ return {
6368
+ columns,
6369
+ mappings: columns.map((column) => buildResolvedMappingFromSummaryColumn(column))
6370
+ };
6371
+ }
6372
+ function buildDefaultSummarySelectColumns(args, index) {
6373
+ const candidates = [];
6374
+ if (args.time_range?.column !== undefined) {
6375
+ candidates.push({
6376
+ selector: args.time_range.column,
6377
+ reason: "auto-selected from time_range.column"
6378
+ });
6379
+ }
6380
+ if (args.amount_column !== undefined) {
6381
+ candidates.push({
6382
+ selector: args.amount_column,
6383
+ reason: "auto-selected from amount_column"
6384
+ });
6385
+ }
6386
+ const idZero = index.byId.get("0");
6387
+ if (idZero?.queId !== undefined && idZero.queId !== null) {
6388
+ candidates.push({
6389
+ selector: normalizeQueId(idZero.queId),
6390
+ reason: "auto-selected default row preview column"
6391
+ });
6392
+ }
6393
+ else {
6394
+ const firstBusinessField = Array.from(index.byId.values()).find((field) => {
6395
+ const normalized = field.queId !== undefined && field.queId !== null ? normalizeQueId(field.queId) : null;
6396
+ return typeof normalized === "number" ? normalized > 0 : Boolean(normalized);
6397
+ });
6398
+ if (firstBusinessField?.queId !== undefined && firstBusinessField.queId !== null) {
6399
+ candidates.push({
6400
+ selector: normalizeQueId(firstBusinessField.queId),
6401
+ reason: "auto-selected first business field for row preview"
6402
+ });
6403
+ }
6404
+ }
6405
+ const deduped = new Set();
6406
+ const selectedCandidates = [];
6407
+ for (const candidate of candidates) {
6408
+ const key = normalizeColumnSelector(candidate.selector);
6409
+ if (deduped.has(key)) {
6410
+ continue;
6411
+ }
6412
+ deduped.add(key);
6413
+ selectedCandidates.push(candidate);
6414
+ if (selectedCandidates.length >= MAX_COLUMN_LIMIT) {
6415
+ break;
6416
+ }
6417
+ }
6418
+ const columns = selectedCandidates.map((candidate) => resolveOutputColumn(candidate.selector, index, "select_columns", "qf_query(summary)"));
6419
+ return {
6420
+ columns,
6421
+ mappings: columns.map((column, indexInArray) => buildResolvedMappingFromSummaryColumn(column, {
6422
+ auto_selected: true,
6423
+ reason: selectedCandidates[indexInArray]?.reason ?? "auto-selected"
6424
+ }))
6425
+ };
6426
+ }
6427
+ function delay(ms) {
6428
+ return new Promise((resolve) => {
6429
+ setTimeout(resolve, ms);
6430
+ });
6431
+ }
6432
+ function extractOperationStatus(operationResult) {
6433
+ const obj = asObject(operationResult);
6434
+ const rawStatus = asNullableString(obj?.status ?? obj?.operationStatus ?? obj?.operation_status ?? obj?.resultStatus ?? null);
6435
+ return rawStatus ? rawStatus.trim().toUpperCase() : null;
6436
+ }
6437
+ function isPendingOperationStatus(status) {
6438
+ if (!status) {
6439
+ return false;
6440
+ }
6441
+ return ["PENDING", "PROCESSING", "RUNNING", "IN_PROGRESS", "QUEUED"].includes(status);
6442
+ }
6443
+ function extractOperationApplyId(operationResult) {
6444
+ const obj = asObject(operationResult);
6445
+ return obj?.applyId ?? obj?.apply_id ?? null;
6446
+ }
6447
+ async function waitForOperationResolution(params) {
6448
+ const deadline = Date.now() + Math.max(1, params.timeoutMs);
6449
+ let lastResult = null;
6450
+ while (Date.now() <= deadline) {
6451
+ const response = await client.getOperation(params.requestId);
6452
+ lastResult = response.result;
6453
+ const status = extractOperationStatus(lastResult);
6454
+ const applyId = extractOperationApplyId(lastResult);
6455
+ if ((status && !isPendingOperationStatus(status)) || applyId !== null) {
6456
+ return {
6457
+ resolved: true,
6458
+ timedOut: false,
6459
+ operationResult: lastResult,
6460
+ applyId
6461
+ };
6462
+ }
6463
+ const remaining = deadline - Date.now();
6464
+ if (remaining <= 0) {
6465
+ break;
6466
+ }
6467
+ await delay(Math.min(WAIT_RESULT_POLL_INTERVAL_MS, remaining));
6468
+ }
6469
+ return {
6470
+ resolved: false,
6471
+ timedOut: true,
6472
+ operationResult: lastResult,
6473
+ applyId: extractOperationApplyId(lastResult)
6474
+ };
5983
6475
  }
5984
6476
  function resolveListItemLimit(params) {
5985
6477
  if (params.total <= 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qingflow-mcp",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",