qingflow-mcp 0.3.13 → 0.3.14

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 +2 -2
  2. package/dist/server.js +795 -103
  3. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -46,6 +46,8 @@ class InputValidationError extends Error {
46
46
  }
47
47
  const FORM_CACHE_TTL_MS = Number(process.env.QINGFLOW_FORM_CACHE_TTL_MS ?? "300000");
48
48
  const formCache = new Map();
49
+ const CONTINUATION_CACHE_TTL_MS = Number(process.env.QINGFLOW_CONTINUATION_CACHE_TTL_MS ?? "900000");
50
+ const continuationCache = new Map();
49
51
  const DEFAULT_PAGE_SIZE = 50;
50
52
  const DEFAULT_SCAN_MAX_PAGES = 10;
51
53
  const DEFAULT_ROW_LIMIT = 200;
@@ -61,7 +63,7 @@ const ADAPTIVE_TARGET_PAGE_MS = toPositiveInt(process.env.QINGFLOW_ADAPTIVE_TARG
61
63
  const MAX_LIST_ITEMS_BYTES = toPositiveInt(process.env.QINGFLOW_LIST_MAX_ITEMS_BYTES) ?? 400000;
62
64
  const REQUEST_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_REQUEST_TIMEOUT_MS) ?? 18000;
63
65
  const EXECUTION_BUDGET_MS = toPositiveInt(process.env.QINGFLOW_EXECUTION_BUDGET_MS) ?? 20000;
64
- const SERVER_VERSION = "0.3.13";
66
+ const SERVER_VERSION = "0.3.14";
65
67
  const accessToken = process.env.QINGFLOW_ACCESS_TOKEN;
66
68
  const baseUrl = process.env.QINGFLOW_BASE_URL;
67
69
  if (!accessToken) {
@@ -227,6 +229,78 @@ const formSuccessOutputSchema = z.object({
227
229
  meta: apiMetaSchema
228
230
  });
229
231
  const formOutputSchema = formSuccessOutputSchema;
232
+ const stringLikeSchema = z.string().min(1);
233
+ const columnSelectorLikeSchema = z.union([stringLikeSchema, z.number().int()]);
234
+ const columnReferenceLikeSchema = z.union([
235
+ columnSelectorLikeSchema,
236
+ z
237
+ .object({
238
+ que_id: columnSelectorLikeSchema.optional(),
239
+ queId: columnSelectorLikeSchema.optional()
240
+ })
241
+ .passthrough()
242
+ ]);
243
+ const booleanLikeSchema = z.union([z.boolean(), stringLikeSchema]);
244
+ function positiveIntLikeSchema(max) {
245
+ const base = z.number().int().positive();
246
+ return z.union([max !== undefined ? base.max(max) : base, stringLikeSchema]);
247
+ }
248
+ function nonNegativeIntLikeSchema(max) {
249
+ const base = z.number().int().nonnegative();
250
+ return z.union([max !== undefined ? base.max(max) : base, stringLikeSchema]);
251
+ }
252
+ const publicSortItemSchema = z
253
+ .object({
254
+ que_id: columnSelectorLikeSchema,
255
+ ascend: booleanLikeSchema.optional()
256
+ })
257
+ .passthrough();
258
+ const publicFilterItemSchema = z
259
+ .object({
260
+ que_id: columnSelectorLikeSchema.optional(),
261
+ search_key: stringLikeSchema.optional(),
262
+ search_keys: z.union([z.array(stringLikeSchema), stringLikeSchema]).optional(),
263
+ min_value: stringLikeSchema.optional(),
264
+ max_value: stringLikeSchema.optional(),
265
+ scope: z.union([z.number().int(), stringLikeSchema]).optional(),
266
+ search_options: z.union([z.array(columnSelectorLikeSchema), stringLikeSchema]).optional(),
267
+ search_user_ids: z.union([z.array(stringLikeSchema), stringLikeSchema]).optional()
268
+ })
269
+ .passthrough();
270
+ const publicTimeRangeSchema = z
271
+ .object({
272
+ column: columnSelectorLikeSchema,
273
+ from: stringLikeSchema.optional(),
274
+ to: stringLikeSchema.optional(),
275
+ timezone: stringLikeSchema.optional()
276
+ })
277
+ .passthrough();
278
+ const publicStatPolicySchema = z
279
+ .object({
280
+ include_negative: booleanLikeSchema.optional(),
281
+ include_null: booleanLikeSchema.optional()
282
+ })
283
+ .passthrough();
284
+ const publicAnswerInputSchema = z
285
+ .object({
286
+ que_id: columnSelectorLikeSchema.optional(),
287
+ queId: columnSelectorLikeSchema.optional(),
288
+ que_title: stringLikeSchema.optional(),
289
+ queTitle: stringLikeSchema.optional(),
290
+ que_type: z.unknown().optional(),
291
+ queType: z.unknown().optional(),
292
+ value: z.unknown().optional(),
293
+ values: z.union([z.array(z.unknown()), stringLikeSchema]).optional(),
294
+ table_values: z.union([z.array(z.array(z.unknown())), stringLikeSchema]).optional(),
295
+ tableValues: z.union([z.array(z.array(z.unknown())), stringLikeSchema]).optional()
296
+ })
297
+ .passthrough();
298
+ const toolSpecInputPublicSchema = z
299
+ .object({
300
+ tool_name: stringLikeSchema.optional(),
301
+ include_all: booleanLikeSchema.optional()
302
+ })
303
+ .passthrough();
230
304
  const toolSpecInputSchema = z.preprocess(normalizeToolSpecInput, z.object({
231
305
  tool_name: z.string().min(1).optional(),
232
306
  include_all: z.boolean().optional()
@@ -250,6 +324,52 @@ const toolSpecOutputSchema = z.object({
250
324
  generated_at: z.string()
251
325
  })
252
326
  });
327
+ const listInputPublicSchema = z
328
+ .object({
329
+ app_key: stringLikeSchema.optional(),
330
+ user_id: stringLikeSchema.optional(),
331
+ page_num: positiveIntLikeSchema().optional(),
332
+ page_token: stringLikeSchema.optional(),
333
+ page_size: positiveIntLikeSchema(200).optional(),
334
+ requested_pages: positiveIntLikeSchema(500).optional(),
335
+ scan_max_pages: positiveIntLikeSchema(500).optional(),
336
+ mode: z
337
+ .enum([
338
+ "todo",
339
+ "done",
340
+ "mine_approved",
341
+ "mine_rejected",
342
+ "mine_draft",
343
+ "mine_need_improve",
344
+ "mine_processing",
345
+ "all",
346
+ "all_approved",
347
+ "all_rejected",
348
+ "all_processing",
349
+ "cc"
350
+ ])
351
+ .optional(),
352
+ type: z.union([z.number().int().min(1).max(12), stringLikeSchema]).optional(),
353
+ keyword: z.string().optional(),
354
+ query_logic: z.union([z.enum(["and", "or"]), stringLikeSchema]).optional(),
355
+ apply_ids: z.union([z.array(z.union([z.string(), z.number()])), stringLikeSchema]).optional(),
356
+ sort: z.union([z.array(publicSortItemSchema), stringLikeSchema]).optional(),
357
+ filters: z.union([z.array(publicFilterItemSchema), stringLikeSchema]).optional(),
358
+ time_range: z.union([publicTimeRangeSchema, stringLikeSchema]).optional(),
359
+ max_rows: positiveIntLikeSchema(200).optional(),
360
+ max_items: positiveIntLikeSchema(200).optional(),
361
+ max_columns: positiveIntLikeSchema(MAX_COLUMN_LIMIT).optional(),
362
+ select_columns: z
363
+ .union([
364
+ z.array(columnReferenceLikeSchema).min(1).max(MAX_COLUMN_LIMIT),
365
+ stringLikeSchema
366
+ ])
367
+ .optional(),
368
+ include_answers: booleanLikeSchema.optional(),
369
+ strict_full: booleanLikeSchema.optional(),
370
+ output_profile: z.union([outputProfileSchema, stringLikeSchema]).optional()
371
+ })
372
+ .passthrough();
253
373
  const listInputSchema = z
254
374
  .preprocess(normalizeListInput, z.object({
255
375
  app_key: z.string().min(1).optional(),
@@ -350,6 +470,19 @@ const listSuccessOutputSchema = z.object({
350
470
  meta: apiMetaSchema.optional()
351
471
  });
352
472
  const listOutputSchema = listSuccessOutputSchema;
473
+ const recordGetInputPublicSchema = z
474
+ .object({
475
+ apply_id: columnSelectorLikeSchema.optional(),
476
+ max_columns: positiveIntLikeSchema(MAX_COLUMN_LIMIT).optional(),
477
+ select_columns: z
478
+ .union([
479
+ z.array(columnReferenceLikeSchema).min(1).max(MAX_COLUMN_LIMIT),
480
+ stringLikeSchema
481
+ ])
482
+ .optional(),
483
+ output_profile: z.union([outputProfileSchema, stringLikeSchema]).optional()
484
+ })
485
+ .passthrough();
353
486
  const recordGetInputSchema = z.preprocess(normalizeRecordGetInput, z.object({
354
487
  apply_id: z.union([z.string().min(1), z.number().int()]),
355
488
  max_columns: z.number().int().positive().max(MAX_COLUMN_LIMIT).optional(),
@@ -382,6 +515,16 @@ const recordGetSuccessOutputSchema = z.object({
382
515
  meta: apiMetaSchema.optional()
383
516
  });
384
517
  const recordGetOutputSchema = recordGetSuccessOutputSchema;
518
+ const createInputPublicSchema = z
519
+ .object({
520
+ app_key: stringLikeSchema.optional(),
521
+ user_id: stringLikeSchema.optional(),
522
+ force_refresh_form: booleanLikeSchema.optional(),
523
+ apply_user: z.union([z.record(z.unknown()), stringLikeSchema]).optional(),
524
+ answers: z.union([z.array(publicAnswerInputSchema), stringLikeSchema]).optional(),
525
+ fields: z.union([z.record(z.unknown()), stringLikeSchema]).optional()
526
+ })
527
+ .passthrough();
385
528
  const createInputSchema = z
386
529
  .object({
387
530
  app_key: z.string().min(1),
@@ -411,6 +554,16 @@ const createSuccessOutputSchema = z.object({
411
554
  meta: apiMetaSchema
412
555
  });
413
556
  const createOutputSchema = createSuccessOutputSchema;
557
+ const updateInputPublicSchema = z
558
+ .object({
559
+ apply_id: columnSelectorLikeSchema.optional(),
560
+ app_key: stringLikeSchema.optional(),
561
+ user_id: stringLikeSchema.optional(),
562
+ force_refresh_form: booleanLikeSchema.optional(),
563
+ answers: z.union([z.array(publicAnswerInputSchema), stringLikeSchema]).optional(),
564
+ fields: z.union([z.record(z.unknown()), stringLikeSchema]).optional()
565
+ })
566
+ .passthrough();
414
567
  const updateInputSchema = z
415
568
  .object({
416
569
  apply_id: z.union([z.string().min(1), z.number().int()]),
@@ -441,6 +594,56 @@ const operationSuccessOutputSchema = z.object({
441
594
  meta: apiMetaSchema
442
595
  });
443
596
  const operationOutputSchema = operationSuccessOutputSchema;
597
+ const queryInputPublicSchema = z
598
+ .object({
599
+ query_mode: z.union([z.enum(["auto", "list", "record", "summary"]), stringLikeSchema]).optional(),
600
+ app_key: stringLikeSchema.optional(),
601
+ apply_id: columnSelectorLikeSchema.optional(),
602
+ user_id: stringLikeSchema.optional(),
603
+ page_num: positiveIntLikeSchema().optional(),
604
+ page_token: stringLikeSchema.optional(),
605
+ page_size: positiveIntLikeSchema(200).optional(),
606
+ requested_pages: positiveIntLikeSchema(500).optional(),
607
+ mode: z
608
+ .enum([
609
+ "todo",
610
+ "done",
611
+ "mine_approved",
612
+ "mine_rejected",
613
+ "mine_draft",
614
+ "mine_need_improve",
615
+ "mine_processing",
616
+ "all",
617
+ "all_approved",
618
+ "all_rejected",
619
+ "all_processing",
620
+ "cc"
621
+ ])
622
+ .optional(),
623
+ type: z.union([z.number().int().min(1).max(12), stringLikeSchema]).optional(),
624
+ keyword: z.string().optional(),
625
+ query_logic: z.union([z.enum(["and", "or"]), stringLikeSchema]).optional(),
626
+ apply_ids: z.union([z.array(z.union([z.string(), z.number()])), stringLikeSchema]).optional(),
627
+ sort: z.union([z.array(publicSortItemSchema), stringLikeSchema]).optional(),
628
+ filters: z.union([z.array(publicFilterItemSchema), stringLikeSchema]).optional(),
629
+ max_rows: positiveIntLikeSchema(200).optional(),
630
+ max_items: positiveIntLikeSchema(200).optional(),
631
+ max_columns: positiveIntLikeSchema(MAX_COLUMN_LIMIT).optional(),
632
+ select_columns: z
633
+ .union([
634
+ z.array(columnReferenceLikeSchema).min(1).max(MAX_COLUMN_LIMIT),
635
+ stringLikeSchema
636
+ ])
637
+ .optional(),
638
+ include_answers: booleanLikeSchema.optional(),
639
+ amount_column: z.union([columnReferenceLikeSchema, stringLikeSchema]).optional(),
640
+ time_range: z.union([publicTimeRangeSchema, stringLikeSchema]).optional(),
641
+ stat_policy: z.union([publicStatPolicySchema, stringLikeSchema]).optional(),
642
+ scan_max_pages: positiveIntLikeSchema(500).optional(),
643
+ strict_full: booleanLikeSchema.optional(),
644
+ output_profile: z.union([outputProfileSchema, stringLikeSchema]).optional()
645
+ })
646
+ .passthrough();
444
647
  const queryInputSchema = z
445
648
  .preprocess(normalizeQueryInput, z.object({
446
649
  query_mode: z.enum(["auto", "list", "record", "summary"]).optional(),
@@ -580,6 +783,58 @@ const querySuccessOutputSchema = z.object({
580
783
  meta: apiMetaSchema.optional()
581
784
  });
582
785
  const queryOutputSchema = querySuccessOutputSchema;
786
+ const aggregateInputPublicSchema = z
787
+ .object({
788
+ app_key: stringLikeSchema.optional(),
789
+ user_id: stringLikeSchema.optional(),
790
+ page_num: positiveIntLikeSchema().optional(),
791
+ page_token: stringLikeSchema.optional(),
792
+ page_size: positiveIntLikeSchema(200).optional(),
793
+ requested_pages: positiveIntLikeSchema(500).optional(),
794
+ scan_max_pages: positiveIntLikeSchema(500).optional(),
795
+ mode: z
796
+ .enum([
797
+ "todo",
798
+ "done",
799
+ "mine_approved",
800
+ "mine_rejected",
801
+ "mine_draft",
802
+ "mine_need_improve",
803
+ "mine_processing",
804
+ "all",
805
+ "all_approved",
806
+ "all_rejected",
807
+ "all_processing",
808
+ "cc"
809
+ ])
810
+ .optional(),
811
+ type: z.union([z.number().int().min(1).max(12), stringLikeSchema]).optional(),
812
+ keyword: z.string().optional(),
813
+ query_logic: z.union([z.enum(["and", "or"]), stringLikeSchema]).optional(),
814
+ apply_ids: z.union([z.array(z.union([z.string(), z.number()])), stringLikeSchema]).optional(),
815
+ sort: z.union([z.array(publicSortItemSchema), stringLikeSchema]).optional(),
816
+ filters: z.union([z.array(publicFilterItemSchema), stringLikeSchema]).optional(),
817
+ time_range: z.union([publicTimeRangeSchema, stringLikeSchema]).optional(),
818
+ group_by: z
819
+ .union([z.array(columnReferenceLikeSchema).min(1).max(20), stringLikeSchema])
820
+ .optional(),
821
+ amount_column: z.union([columnReferenceLikeSchema, stringLikeSchema]).optional(),
822
+ amount_columns: z
823
+ .union([z.array(columnReferenceLikeSchema).min(1).max(5), stringLikeSchema])
824
+ .optional(),
825
+ metrics: z
826
+ .union([
827
+ z.array(z.enum(["count", "sum", "avg", "min", "max"])).min(1).max(5),
828
+ stringLikeSchema
829
+ ])
830
+ .optional(),
831
+ time_bucket: z.union([z.enum(["day", "week", "month"]), stringLikeSchema]).optional(),
832
+ stat_policy: z.union([publicStatPolicySchema, stringLikeSchema]).optional(),
833
+ max_groups: positiveIntLikeSchema(2000).optional(),
834
+ strict_full: booleanLikeSchema.optional(),
835
+ output_profile: z.union([outputProfileSchema, stringLikeSchema]).optional()
836
+ })
837
+ .passthrough();
583
838
  const aggregateInputSchema = z
584
839
  .preprocess(normalizeAggregateInput, z.object({
585
840
  app_key: z.string().min(1),
@@ -695,6 +950,15 @@ const aggregateOutputSchema = z.object({
695
950
  ...queryContractFields,
696
951
  meta: apiMetaSchema.optional()
697
952
  });
953
+ const fieldResolveInputPublicSchema = z
954
+ .object({
955
+ app_key: stringLikeSchema.optional(),
956
+ query: columnSelectorLikeSchema.optional(),
957
+ queries: z.union([z.array(columnSelectorLikeSchema).min(1).max(50), stringLikeSchema]).optional(),
958
+ top_k: positiveIntLikeSchema(10).optional(),
959
+ fuzzy: booleanLikeSchema.optional()
960
+ })
961
+ .passthrough();
698
962
  const fieldResolveInputSchema = z.preprocess(normalizeFieldResolveInput, z.object({
699
963
  app_key: z.string().min(1),
700
964
  query: z.union([z.string().min(1), z.number().int()]).optional(),
@@ -722,6 +986,14 @@ const fieldResolveOutputSchema = z.object({
722
986
  }),
723
987
  meta: apiMetaSchema
724
988
  });
989
+ const queryPlanInputPublicSchema = z
990
+ .object({
991
+ tool: stringLikeSchema.optional(),
992
+ arguments: z.union([z.record(z.unknown()), stringLikeSchema]).optional(),
993
+ resolve_fields: booleanLikeSchema.optional(),
994
+ probe: booleanLikeSchema.optional()
995
+ })
996
+ .passthrough();
725
997
  const queryPlanInputSchema = z.preprocess(normalizeQueryPlanInput, z.object({
726
998
  tool: z.string().min(1),
727
999
  arguments: z.record(z.unknown()).optional(),
@@ -761,13 +1033,31 @@ const queryPlanOutputSchema = z.object({
761
1033
  page_amount: z.number().int().nonnegative().nullable()
762
1034
  })
763
1035
  .nullable()
764
- })
1036
+ }),
1037
+ ready_for_final_conclusion: z.boolean(),
1038
+ final_conclusion_blockers: z.array(z.string()),
1039
+ recommended_next_actions: z.array(z.string())
765
1040
  }),
766
1041
  meta: z.object({
767
1042
  version: z.string(),
768
1043
  generated_at: z.string()
769
1044
  })
770
1045
  });
1046
+ const batchGetInputPublicSchema = z
1047
+ .object({
1048
+ app_key: stringLikeSchema.optional(),
1049
+ user_id: stringLikeSchema.optional(),
1050
+ apply_ids: z.union([z.array(columnSelectorLikeSchema).min(1).max(200), stringLikeSchema]).optional(),
1051
+ select_columns: z
1052
+ .union([
1053
+ z.array(columnReferenceLikeSchema).min(1).max(MAX_COLUMN_LIMIT),
1054
+ stringLikeSchema
1055
+ ])
1056
+ .optional(),
1057
+ max_columns: positiveIntLikeSchema(MAX_COLUMN_LIMIT).optional(),
1058
+ output_profile: z.union([outputProfileSchema, stringLikeSchema]).optional()
1059
+ })
1060
+ .passthrough();
771
1061
  const batchGetInputSchema = z.preprocess(normalizeBatchGetInput, z.object({
772
1062
  app_key: z.string().min(1),
773
1063
  user_id: z.string().min(1).optional(),
@@ -799,6 +1089,52 @@ const batchGetOutputSchema = z.object({
799
1089
  ...queryContractFields,
800
1090
  meta: apiMetaSchema.optional()
801
1091
  });
1092
+ const exportInputPublicSchema = z
1093
+ .object({
1094
+ app_key: stringLikeSchema.optional(),
1095
+ user_id: stringLikeSchema.optional(),
1096
+ page_num: positiveIntLikeSchema().optional(),
1097
+ page_token: stringLikeSchema.optional(),
1098
+ page_size: positiveIntLikeSchema(200).optional(),
1099
+ requested_pages: positiveIntLikeSchema(500).optional(),
1100
+ scan_max_pages: positiveIntLikeSchema(500).optional(),
1101
+ mode: z
1102
+ .enum([
1103
+ "todo",
1104
+ "done",
1105
+ "mine_approved",
1106
+ "mine_rejected",
1107
+ "mine_draft",
1108
+ "mine_need_improve",
1109
+ "mine_processing",
1110
+ "all",
1111
+ "all_approved",
1112
+ "all_rejected",
1113
+ "all_processing",
1114
+ "cc"
1115
+ ])
1116
+ .optional(),
1117
+ type: z.union([z.number().int().min(1).max(12), stringLikeSchema]).optional(),
1118
+ keyword: z.string().optional(),
1119
+ query_logic: z.union([z.enum(["and", "or"]), stringLikeSchema]).optional(),
1120
+ apply_ids: z.union([z.array(z.union([z.string(), z.number()])), stringLikeSchema]).optional(),
1121
+ sort: z.union([z.array(publicSortItemSchema), stringLikeSchema]).optional(),
1122
+ filters: z.union([z.array(publicFilterItemSchema), stringLikeSchema]).optional(),
1123
+ time_range: z.union([publicTimeRangeSchema, stringLikeSchema]).optional(),
1124
+ max_rows: positiveIntLikeSchema(EXPORT_MAX_ROWS).optional(),
1125
+ max_columns: positiveIntLikeSchema(MAX_COLUMN_LIMIT).optional(),
1126
+ select_columns: z
1127
+ .union([
1128
+ z.array(columnReferenceLikeSchema).min(1).max(MAX_COLUMN_LIMIT),
1129
+ stringLikeSchema
1130
+ ])
1131
+ .optional(),
1132
+ strict_full: booleanLikeSchema.optional(),
1133
+ output_profile: z.union([outputProfileSchema, stringLikeSchema]).optional(),
1134
+ export_dir: stringLikeSchema.optional(),
1135
+ file_name: stringLikeSchema.optional()
1136
+ })
1137
+ .passthrough();
802
1138
  const exportInputSchema = z.preprocess(normalizeExportInput, z.object({
803
1139
  app_key: z.string().min(1).optional(),
804
1140
  user_id: z.string().min(1).optional(),
@@ -895,7 +1231,7 @@ const exportOutputSchema = z.object({
895
1231
  server.registerTool("qf_tool_spec_get", {
896
1232
  title: "Qingflow Tool Spec Get",
897
1233
  description: "Return MCP tool parameter requirements, limits, aliases and minimal examples for agent prompt grounding.",
898
- inputSchema: toolSpecInputSchema,
1234
+ inputSchema: toolSpecInputPublicSchema,
899
1235
  outputSchema: toolSpecOutputSchema,
900
1236
  annotations: {
901
1237
  readOnlyHint: true,
@@ -903,9 +1239,10 @@ server.registerTool("qf_tool_spec_get", {
903
1239
  }
904
1240
  }, async (args) => {
905
1241
  try {
1242
+ const parsedArgs = toolSpecInputSchema.parse(args);
906
1243
  const allSpecs = buildToolSpecCatalog();
907
- const requested = args.tool_name?.trim() ?? null;
908
- const includeAll = args.include_all ?? false;
1244
+ const requested = parsedArgs.tool_name?.trim() ?? null;
1245
+ const includeAll = parsedArgs.include_all ?? false;
909
1246
  const normalizedRequested = requested?.toLowerCase() ?? null;
910
1247
  let tools = allSpecs;
911
1248
  if (normalizedRequested && !includeAll) {
@@ -1018,7 +1355,7 @@ server.registerTool("qf_form_get", {
1018
1355
  server.registerTool("qf_field_resolve", {
1019
1356
  title: "Qingflow Field Resolve",
1020
1357
  description: "Resolve natural language field names/aliases into stable que_id mappings for one app.",
1021
- inputSchema: fieldResolveInputSchema,
1358
+ inputSchema: fieldResolveInputPublicSchema,
1022
1359
  outputSchema: fieldResolveOutputSchema,
1023
1360
  annotations: {
1024
1361
  readOnlyHint: true,
@@ -1026,8 +1363,9 @@ server.registerTool("qf_field_resolve", {
1026
1363
  }
1027
1364
  }, async (args) => {
1028
1365
  try {
1029
- const payload = await executeFieldResolve(args);
1030
- return okResult(payload, `Resolved fields for ${args.app_key}`);
1366
+ const parsedArgs = fieldResolveInputSchema.parse(args);
1367
+ const payload = await executeFieldResolve(parsedArgs);
1368
+ return okResult(payload, `Resolved fields for ${parsedArgs.app_key}`);
1031
1369
  }
1032
1370
  catch (error) {
1033
1371
  return errorResult(error);
@@ -1036,7 +1374,7 @@ server.registerTool("qf_field_resolve", {
1036
1374
  server.registerTool("qf_query_plan", {
1037
1375
  title: "Qingflow Query Plan",
1038
1376
  description: "Preflight query arguments: normalize inputs, validate required fields, resolve mappings and estimate scan limits before execution.",
1039
- inputSchema: queryPlanInputSchema,
1377
+ inputSchema: queryPlanInputPublicSchema,
1040
1378
  outputSchema: queryPlanOutputSchema,
1041
1379
  annotations: {
1042
1380
  readOnlyHint: true,
@@ -1044,8 +1382,9 @@ server.registerTool("qf_query_plan", {
1044
1382
  }
1045
1383
  }, async (args) => {
1046
1384
  try {
1047
- const payload = await executeQueryPlan(args);
1048
- return okResult(payload, `Planned ${args.tool}`);
1385
+ const parsedArgs = queryPlanInputSchema.parse(args);
1386
+ const payload = await executeQueryPlan(parsedArgs);
1387
+ return okResult(payload, `Planned ${parsedArgs.tool}`);
1049
1388
  }
1050
1389
  catch (error) {
1051
1390
  return errorResult(error);
@@ -1054,7 +1393,7 @@ server.registerTool("qf_query_plan", {
1054
1393
  server.registerTool("qf_records_list", {
1055
1394
  title: "Qingflow Records List",
1056
1395
  description: "List records with pagination, filters and sorting.",
1057
- inputSchema: listInputSchema,
1396
+ inputSchema: listInputPublicSchema,
1058
1397
  outputSchema: listOutputSchema,
1059
1398
  annotations: {
1060
1399
  readOnlyHint: true,
@@ -1062,7 +1401,8 @@ server.registerTool("qf_records_list", {
1062
1401
  }
1063
1402
  }, async (args) => {
1064
1403
  try {
1065
- const executed = await executeRecordsList(args);
1404
+ const parsedArgs = listInputSchema.parse(args);
1405
+ const executed = await executeRecordsList(parsedArgs);
1066
1406
  return okResult(executed.payload, executed.message);
1067
1407
  }
1068
1408
  catch (error) {
@@ -1072,7 +1412,7 @@ server.registerTool("qf_records_list", {
1072
1412
  server.registerTool("qf_record_get", {
1073
1413
  title: "Qingflow Record Get",
1074
1414
  description: "Get one record by applyId.",
1075
- inputSchema: recordGetInputSchema,
1415
+ inputSchema: recordGetInputPublicSchema,
1076
1416
  outputSchema: recordGetOutputSchema,
1077
1417
  annotations: {
1078
1418
  readOnlyHint: true,
@@ -1080,7 +1420,8 @@ server.registerTool("qf_record_get", {
1080
1420
  }
1081
1421
  }, async (args) => {
1082
1422
  try {
1083
- const executed = await executeRecordGet(args);
1423
+ const parsedArgs = recordGetInputSchema.parse(args);
1424
+ const executed = await executeRecordGet(parsedArgs);
1084
1425
  return okResult(executed.payload, executed.message);
1085
1426
  }
1086
1427
  catch (error) {
@@ -1090,7 +1431,7 @@ server.registerTool("qf_record_get", {
1090
1431
  server.registerTool("qf_records_batch_get", {
1091
1432
  title: "Qingflow Records Batch Get",
1092
1433
  description: "Fetch multiple records by apply_ids in one call and return strict flat rows.",
1093
- inputSchema: batchGetInputSchema,
1434
+ inputSchema: batchGetInputPublicSchema,
1094
1435
  outputSchema: batchGetOutputSchema,
1095
1436
  annotations: {
1096
1437
  readOnlyHint: true,
@@ -1098,7 +1439,8 @@ server.registerTool("qf_records_batch_get", {
1098
1439
  }
1099
1440
  }, async (args) => {
1100
1441
  try {
1101
- const payload = await executeRecordsBatchGet(args);
1442
+ const parsedArgs = batchGetInputSchema.parse(args);
1443
+ const payload = await executeRecordsBatchGet(parsedArgs);
1102
1444
  return okResult(payload.payload, payload.message);
1103
1445
  }
1104
1446
  catch (error) {
@@ -1108,7 +1450,7 @@ server.registerTool("qf_records_batch_get", {
1108
1450
  server.registerTool("qf_export_csv", {
1109
1451
  title: "Qingflow Export CSV",
1110
1452
  description: "Export list query result to a CSV file and return file path + summary instead of large inline payloads.",
1111
- inputSchema: exportInputSchema,
1453
+ inputSchema: exportInputPublicSchema,
1112
1454
  outputSchema: exportOutputSchema,
1113
1455
  annotations: {
1114
1456
  readOnlyHint: true,
@@ -1116,7 +1458,8 @@ server.registerTool("qf_export_csv", {
1116
1458
  }
1117
1459
  }, async (args) => {
1118
1460
  try {
1119
- const executed = await executeRecordsExport("csv", args);
1461
+ const parsedArgs = exportInputSchema.parse(args);
1462
+ const executed = await executeRecordsExport("csv", parsedArgs);
1120
1463
  return okResult(executed.payload, executed.message);
1121
1464
  }
1122
1465
  catch (error) {
@@ -1126,7 +1469,7 @@ server.registerTool("qf_export_csv", {
1126
1469
  server.registerTool("qf_export_json", {
1127
1470
  title: "Qingflow Export JSON",
1128
1471
  description: "Export list query result to a JSON file and return file path + summary instead of large inline payloads.",
1129
- inputSchema: exportInputSchema,
1472
+ inputSchema: exportInputPublicSchema,
1130
1473
  outputSchema: exportOutputSchema,
1131
1474
  annotations: {
1132
1475
  readOnlyHint: true,
@@ -1134,7 +1477,8 @@ server.registerTool("qf_export_json", {
1134
1477
  }
1135
1478
  }, async (args) => {
1136
1479
  try {
1137
- const executed = await executeRecordsExport("json", args);
1480
+ const parsedArgs = exportInputSchema.parse(args);
1481
+ const executed = await executeRecordsExport("json", parsedArgs);
1138
1482
  return okResult(executed.payload, executed.message);
1139
1483
  }
1140
1484
  catch (error) {
@@ -1144,7 +1488,7 @@ server.registerTool("qf_export_json", {
1144
1488
  server.registerTool("qf_query", {
1145
1489
  title: "Qingflow Unified Query",
1146
1490
  description: "Unified read entry for list/record/summary. Use query_mode=auto to route automatically.",
1147
- inputSchema: queryInputSchema,
1491
+ inputSchema: queryInputPublicSchema,
1148
1492
  outputSchema: queryOutputSchema,
1149
1493
  annotations: {
1150
1494
  readOnlyHint: true,
@@ -1152,9 +1496,10 @@ server.registerTool("qf_query", {
1152
1496
  }
1153
1497
  }, async (args) => {
1154
1498
  try {
1155
- const routedMode = resolveQueryMode(args);
1499
+ const parsedArgs = queryInputSchema.parse(args);
1500
+ const routedMode = resolveQueryMode(parsedArgs);
1156
1501
  if (routedMode === "record") {
1157
- const recordArgs = buildRecordGetArgsFromQuery(args);
1502
+ const recordArgs = buildRecordGetArgsFromQuery(parsedArgs);
1158
1503
  const executed = await executeRecordGet(recordArgs);
1159
1504
  const completeness = executed.completeness;
1160
1505
  const evidence = executed.evidence;
@@ -1183,7 +1528,7 @@ server.registerTool("qf_query", {
1183
1528
  }, executed.message);
1184
1529
  }
1185
1530
  if (routedMode === "summary") {
1186
- const executed = await executeRecordsSummary(args);
1531
+ const executed = await executeRecordsSummary(parsedArgs);
1187
1532
  const completeness = executed.completeness;
1188
1533
  const evidence = executed.evidence;
1189
1534
  return okResult({
@@ -1210,7 +1555,7 @@ server.registerTool("qf_query", {
1210
1555
  : {})
1211
1556
  }, executed.message);
1212
1557
  }
1213
- const listArgs = buildListArgsFromQuery(args);
1558
+ const listArgs = buildListArgsFromQuery(parsedArgs);
1214
1559
  const executed = await executeRecordsList(listArgs);
1215
1560
  const completeness = executed.completeness;
1216
1561
  const evidence = executed.evidence;
@@ -1245,7 +1590,7 @@ server.registerTool("qf_query", {
1245
1590
  server.registerTool("qf_record_create", {
1246
1591
  title: "Qingflow Record Create",
1247
1592
  description: "Create one record. Supports explicit answers and ergonomic fields mapping (title or queId).",
1248
- inputSchema: createInputSchema,
1593
+ inputSchema: createInputPublicSchema,
1249
1594
  outputSchema: createOutputSchema,
1250
1595
  annotations: {
1251
1596
  readOnlyHint: false,
@@ -1253,22 +1598,23 @@ server.registerTool("qf_record_create", {
1253
1598
  }
1254
1599
  }, async (args) => {
1255
1600
  try {
1256
- const form = needsFormResolution(args.fields) || Boolean(args.force_refresh_form)
1257
- ? await getFormCached(args.app_key, args.user_id, Boolean(args.force_refresh_form))
1601
+ const parsedArgs = createInputSchema.parse(args);
1602
+ const form = needsFormResolution(parsedArgs.fields) || Boolean(parsedArgs.force_refresh_form)
1603
+ ? await getFormCached(parsedArgs.app_key, parsedArgs.user_id, Boolean(parsedArgs.force_refresh_form))
1258
1604
  : null;
1259
1605
  const normalizedAnswers = resolveAnswers({
1260
- explicitAnswers: args.answers,
1261
- fields: args.fields,
1606
+ explicitAnswers: parsedArgs.answers,
1607
+ fields: parsedArgs.fields,
1262
1608
  form: form?.result
1263
1609
  });
1264
1610
  const payload = {
1265
1611
  answers: normalizedAnswers
1266
1612
  };
1267
- if (args.apply_user) {
1268
- payload.applyUser = args.apply_user;
1613
+ if (parsedArgs.apply_user) {
1614
+ payload.applyUser = parsedArgs.apply_user;
1269
1615
  }
1270
- const response = await client.createRecord(args.app_key, payload, {
1271
- userId: args.user_id
1616
+ const response = await client.createRecord(parsedArgs.app_key, payload, {
1617
+ userId: parsedArgs.user_id
1272
1618
  });
1273
1619
  const result = asObject(response.result);
1274
1620
  return okResult({
@@ -1279,7 +1625,7 @@ server.registerTool("qf_record_create", {
1279
1625
  async_hint: "Use qf_operation_get with request_id when apply_id is null."
1280
1626
  },
1281
1627
  meta: buildMeta(response)
1282
- }, `Create request sent for app ${args.app_key}`);
1628
+ }, `Create request sent for app ${parsedArgs.app_key}`);
1283
1629
  }
1284
1630
  catch (error) {
1285
1631
  return errorResult(error);
@@ -1288,7 +1634,7 @@ server.registerTool("qf_record_create", {
1288
1634
  server.registerTool("qf_record_update", {
1289
1635
  title: "Qingflow Record Update",
1290
1636
  description: "Patch one record by applyId with explicit answers or ergonomic fields mapping.",
1291
- inputSchema: updateInputSchema,
1637
+ inputSchema: updateInputPublicSchema,
1292
1638
  outputSchema: updateOutputSchema,
1293
1639
  annotations: {
1294
1640
  readOnlyHint: false,
@@ -1296,19 +1642,20 @@ server.registerTool("qf_record_update", {
1296
1642
  }
1297
1643
  }, async (args) => {
1298
1644
  try {
1299
- const requiresForm = needsFormResolution(args.fields);
1300
- if (requiresForm && !args.app_key) {
1645
+ const parsedArgs = updateInputSchema.parse(args);
1646
+ const requiresForm = needsFormResolution(parsedArgs.fields);
1647
+ if (requiresForm && !parsedArgs.app_key) {
1301
1648
  throw new Error("app_key is required when fields uses title-based keys");
1302
1649
  }
1303
- const form = requiresForm && args.app_key
1304
- ? await getFormCached(args.app_key, args.user_id, Boolean(args.force_refresh_form))
1650
+ const form = requiresForm && parsedArgs.app_key
1651
+ ? await getFormCached(parsedArgs.app_key, parsedArgs.user_id, Boolean(parsedArgs.force_refresh_form))
1305
1652
  : null;
1306
1653
  const normalizedAnswers = resolveAnswers({
1307
- explicitAnswers: args.answers,
1308
- fields: args.fields,
1654
+ explicitAnswers: parsedArgs.answers,
1655
+ fields: parsedArgs.fields,
1309
1656
  form: form?.result
1310
1657
  });
1311
- const response = await client.updateRecord(String(args.apply_id), { answers: normalizedAnswers }, { userId: args.user_id });
1658
+ const response = await client.updateRecord(String(parsedArgs.apply_id), { answers: normalizedAnswers }, { userId: parsedArgs.user_id });
1312
1659
  const result = asObject(response.result);
1313
1660
  return okResult({
1314
1661
  ok: true,
@@ -1317,7 +1664,7 @@ server.registerTool("qf_record_update", {
1317
1664
  async_hint: "Use qf_operation_get with request_id to fetch update result when needed."
1318
1665
  },
1319
1666
  meta: buildMeta(response)
1320
- }, `Update request sent for apply ${String(args.apply_id)}`);
1667
+ }, `Update request sent for apply ${String(parsedArgs.apply_id)}`);
1321
1668
  }
1322
1669
  catch (error) {
1323
1670
  return errorResult(error);
@@ -1351,7 +1698,7 @@ server.registerTool("qf_operation_get", {
1351
1698
  server.registerTool("qf_records_aggregate", {
1352
1699
  title: "Qingflow Records Aggregate",
1353
1700
  description: "Aggregate records by group_by columns with optional amount metrics. Designed for deterministic, auditable statistics.",
1354
- inputSchema: aggregateInputSchema,
1701
+ inputSchema: aggregateInputPublicSchema,
1355
1702
  outputSchema: aggregateOutputSchema,
1356
1703
  annotations: {
1357
1704
  readOnlyHint: true,
@@ -1359,7 +1706,8 @@ server.registerTool("qf_records_aggregate", {
1359
1706
  }
1360
1707
  }, async (args) => {
1361
1708
  try {
1362
- const executed = await executeRecordsAggregate(args);
1709
+ const parsedArgs = aggregateInputSchema.parse(args);
1710
+ const executed = await executeRecordsAggregate(parsedArgs);
1363
1711
  return okResult(executed.payload, executed.message);
1364
1712
  }
1365
1713
  catch (error) {
@@ -1653,6 +2001,9 @@ const COMMON_INPUT_ALIASES = {
1653
2001
  pageNum: "page_num",
1654
2002
  pageSize: "page_size",
1655
2003
  pageToken: "page_token",
2004
+ rawNextPageToken: "page_token",
2005
+ raw_next_page_token: "page_token",
2006
+ rawPageToken: "page_token",
1656
2007
  requestedPages: "requested_pages",
1657
2008
  scanMaxPages: "scan_max_pages",
1658
2009
  queryMode: "query_mode",
@@ -2801,12 +3152,104 @@ function decodeContinuationToken(token) {
2801
3152
  if (!appKey || !nextPageNum || !pageSize) {
2802
3153
  throw new Error("Invalid page_token payload");
2803
3154
  }
3155
+ const resumeKind = obj?.resume_kind === "summary" || obj?.resume_kind === "aggregate"
3156
+ ? obj.resume_kind
3157
+ : undefined;
3158
+ const resumeId = asNullableString(obj?.resume_id) ?? undefined;
2804
3159
  return {
2805
3160
  app_key: appKey,
2806
3161
  next_page_num: nextPageNum,
2807
- page_size: pageSize
3162
+ page_size: pageSize,
3163
+ ...(resumeKind ? { resume_kind: resumeKind } : {}),
3164
+ ...(resumeId ? { resume_id: resumeId } : {})
2808
3165
  };
2809
3166
  }
3167
+ function resolveContinuationPayload(pageToken, appKey) {
3168
+ if (!pageToken) {
3169
+ return null;
3170
+ }
3171
+ const payload = decodeContinuationToken(pageToken);
3172
+ if (payload.app_key !== appKey) {
3173
+ throw new Error(`page_token app_key mismatch: token for ${payload.app_key}, request for ${appKey}`);
3174
+ }
3175
+ return payload;
3176
+ }
3177
+ function buildQueryFingerprint(value) {
3178
+ return stableJson(value);
3179
+ }
3180
+ function loadContinuationState(kind, payload, queryFingerprint, tool) {
3181
+ if (!payload) {
3182
+ return null;
3183
+ }
3184
+ if (payload.resume_kind !== kind || !payload.resume_id) {
3185
+ throw new InputValidationError({
3186
+ message: `${tool} received a page_token that cannot resume aggregated state`,
3187
+ errorCode: "INVALID_PAGE_TOKEN",
3188
+ fixHint: `Reuse the raw_next_page_token returned by ${tool}, or restart the query without page_token.`,
3189
+ details: {
3190
+ tool,
3191
+ expected_resume_kind: kind,
3192
+ received_resume_kind: payload.resume_kind ?? null
3193
+ }
3194
+ });
3195
+ }
3196
+ const state = getContinuationState(kind, payload.resume_id);
3197
+ if (!state) {
3198
+ throw new InputValidationError({
3199
+ message: `${tool} continuation state expired`,
3200
+ errorCode: "CONTINUATION_EXPIRED",
3201
+ fixHint: "Restart the query without page_token to rebuild the aggregate from page 1.",
3202
+ details: {
3203
+ tool,
3204
+ resume_id: payload.resume_id
3205
+ }
3206
+ });
3207
+ }
3208
+ if (state.query_fingerprint !== queryFingerprint) {
3209
+ throw new InputValidationError({
3210
+ message: `${tool} page_token no longer matches the current query arguments`,
3211
+ errorCode: "CONTINUATION_MISMATCH",
3212
+ fixHint: "When continuing a summary/aggregate query, keep app_key, filters, time_range, grouping, selected columns and stat options unchanged.",
3213
+ details: {
3214
+ tool,
3215
+ resume_id: payload.resume_id
3216
+ }
3217
+ });
3218
+ }
3219
+ return {
3220
+ resumeId: payload.resume_id,
3221
+ state
3222
+ };
3223
+ }
3224
+ function setContinuationState(kind, state, resumeId) {
3225
+ const id = resumeId ?? randomUUID();
3226
+ continuationCache.set(id, {
3227
+ kind,
3228
+ state: state,
3229
+ expiresAt: Date.now() + CONTINUATION_CACHE_TTL_MS
3230
+ });
3231
+ return id;
3232
+ }
3233
+ function getContinuationState(kind, resumeId) {
3234
+ const hit = continuationCache.get(resumeId);
3235
+ if (!hit) {
3236
+ return null;
3237
+ }
3238
+ if (hit.expiresAt <= Date.now()) {
3239
+ continuationCache.delete(resumeId);
3240
+ return null;
3241
+ }
3242
+ if (hit.kind !== kind) {
3243
+ throw new Error(`page_token continuation kind mismatch: expected ${kind}, got ${hit.kind}`);
3244
+ }
3245
+ return hit.state;
3246
+ }
3247
+ function deleteContinuationState(resumeId) {
3248
+ if (!resumeId) {
3249
+ return;
3250
+ }
3251
+ continuationCache.delete(resumeId);
3252
+ }
2810
3253
  function isExecutionBudgetExceeded(startedAt) {
2811
3254
  return Date.now() - startedAt >= EXECUTION_BUDGET_MS;
2812
3255
  }
@@ -3221,6 +3664,54 @@ async function estimatePlanExecution(params) {
3221
3664
  probe: probeResult
3222
3665
  };
3223
3666
  }
3667
+ function assessPlanReadiness(params) {
3668
+ const blockers = [];
3669
+ const actions = [];
3670
+ if (!params.validation.valid) {
3671
+ blockers.push("arguments are not valid");
3672
+ actions.push("Fix missing_required and warnings before execution.");
3673
+ }
3674
+ const unresolved = params.fieldMapping.filter((item) => !item.resolved);
3675
+ if (unresolved.length > 0) {
3676
+ blockers.push(`unresolved fields: ${unresolved.map((item) => `${item.role}:${item.requested}`).join(", ")}`);
3677
+ actions.push("Use qf_form_get or qf_field_resolve to resolve field ids before execution.");
3678
+ }
3679
+ const tool = params.tool;
3680
+ const routedMode = tool === "qf_query"
3681
+ ? resolveQueryMode(params.normalizedArguments)
3682
+ : null;
3683
+ const strictFull = params.normalizedArguments.strict_full === true;
3684
+ const scanLimit = resolveScanLimit(params.estimate.requested_pages ?? 1, params.estimate.scan_max_pages ?? params.estimate.requested_pages ?? 1);
3685
+ const pageSize = params.estimate.page_size ?? null;
3686
+ const probeResultAmount = params.estimate.probe?.result_amount ?? null;
3687
+ const requiredPagesFromProbe = probeResultAmount !== null && pageSize !== null
3688
+ ? Math.max(1, Math.ceil(probeResultAmount / pageSize))
3689
+ : null;
3690
+ if (tool === "qf_records_list" || (tool === "qf_query" && routedMode === "list")) {
3691
+ blockers.push("list mode is not a safe final-analysis endpoint");
3692
+ actions.push("Use qf_query(summary) or qf_records_aggregate for final statistics.");
3693
+ }
3694
+ if (tool === "qf_records_batch_get" || tool === "qf_export_csv" || tool === "qf_export_json") {
3695
+ blockers.push(`${tool} is a data retrieval/export endpoint, not a final-analysis endpoint`);
3696
+ actions.push("Use qf_query(summary) or qf_records_aggregate for final statistics.");
3697
+ }
3698
+ if (tool === "qf_records_aggregate" || (tool === "qf_query" && routedMode === "summary")) {
3699
+ if (!strictFull) {
3700
+ blockers.push("strict_full must be true for final conclusions");
3701
+ actions.push("Set strict_full=true so incomplete raw scans fail with NEED_MORE_DATA.");
3702
+ }
3703
+ if (requiredPagesFromProbe !== null && scanLimit < requiredPagesFromProbe) {
3704
+ blockers.push(`scan budget is smaller than estimated page count (${scanLimit} < ${requiredPagesFromProbe})`);
3705
+ actions.push("Increase requested_pages/scan_max_pages or continue with raw_next_page_token.");
3706
+ }
3707
+ }
3708
+ actions.push("After execution, still verify completeness.raw_scan_complete=true before concluding.");
3709
+ return {
3710
+ ready_for_final_conclusion: blockers.length === 0,
3711
+ final_conclusion_blockers: uniqueStringList(blockers),
3712
+ recommended_next_actions: uniqueStringList(actions)
3713
+ };
3714
+ }
3224
3715
  function scoreFieldMatches(requested, fields, fuzzy, topK) {
3225
3716
  const normalizedRequested = requested.trim().toLowerCase();
3226
3717
  const requestedIsId = isNumericKey(normalizedRequested);
@@ -3669,6 +4160,18 @@ async function executeQueryPlan(args) {
3669
4160
  probe: args.probe !== false,
3670
4161
  warnings: validation.warnings
3671
4162
  });
4163
+ const readiness = assessPlanReadiness({
4164
+ tool: normalizedTool,
4165
+ normalizedArguments,
4166
+ validation,
4167
+ fieldMapping: fieldMapping.map((item) => ({
4168
+ role: item.role,
4169
+ requested: item.requested,
4170
+ resolved: item.resolved,
4171
+ reason: item.reason
4172
+ })),
4173
+ estimate
4174
+ });
3672
4175
  return {
3673
4176
  ok: true,
3674
4177
  data: {
@@ -3676,7 +4179,10 @@ async function executeQueryPlan(args) {
3676
4179
  normalized_arguments: normalizedArguments,
3677
4180
  validation,
3678
4181
  field_mapping: fieldMapping,
3679
- estimate
4182
+ estimate,
4183
+ ready_for_final_conclusion: readiness.ready_for_final_conclusion,
4184
+ final_conclusion_blockers: readiness.final_conclusion_blockers,
4185
+ recommended_next_actions: readiness.recommended_next_actions
3680
4186
  },
3681
4187
  meta: {
3682
4188
  version: SERVER_VERSION,
@@ -4327,6 +4833,108 @@ async function executeRecordGet(args) {
4327
4833
  outputProfile
4328
4834
  };
4329
4835
  }
4836
+ function normalizeSortForFingerprint(sort) {
4837
+ return (sort ?? []).map((item) => ({
4838
+ que_id: String(item.que_id),
4839
+ ascend: item.ascend !== false
4840
+ }));
4841
+ }
4842
+ function cloneByDayBuckets(source) {
4843
+ return source.map(([day, bucket]) => [day, { count: bucket.count, amount: bucket.amount }]);
4844
+ }
4845
+ function cloneMetricAccumulator(accumulator) {
4846
+ return {
4847
+ count: accumulator.count,
4848
+ sum: accumulator.sum,
4849
+ min: accumulator.min,
4850
+ max: accumulator.max
4851
+ };
4852
+ }
4853
+ function serializeMetricAccumulatorMap(map) {
4854
+ return Array.from(map.entries()).map(([key, value]) => [key, cloneMetricAccumulator(value)]);
4855
+ }
4856
+ function restoreMetricAccumulatorMap(values) {
4857
+ return new Map(values.map(([key, value]) => [
4858
+ key,
4859
+ {
4860
+ count: value.count,
4861
+ sum: value.sum,
4862
+ min: value.min,
4863
+ max: value.max
4864
+ }
4865
+ ]));
4866
+ }
4867
+ function serializeAggregateGroupStats(groupStats) {
4868
+ return Array.from(groupStats.entries()).map(([key, bucket]) => ({
4869
+ key,
4870
+ group: bucket.group,
4871
+ count: bucket.count,
4872
+ amount: bucket.amount,
4873
+ metrics: serializeMetricAccumulatorMap(bucket.metrics)
4874
+ }));
4875
+ }
4876
+ function restoreAggregateGroupStats(values) {
4877
+ return new Map(values.map((item) => [
4878
+ item.key,
4879
+ {
4880
+ group: item.group,
4881
+ count: item.count,
4882
+ amount: item.amount,
4883
+ metrics: restoreMetricAccumulatorMap(item.metrics)
4884
+ }
4885
+ ]));
4886
+ }
4887
+ function buildSummaryContinuationFingerprint(params) {
4888
+ return buildQueryFingerprint({
4889
+ kind: "summary",
4890
+ app_key: params.app_key,
4891
+ mode: params.mode ?? null,
4892
+ type: params.type ?? null,
4893
+ keyword: params.keyword ?? null,
4894
+ query_logic: params.query_logic ?? null,
4895
+ apply_ids: uniqueStringList((params.apply_ids ?? []).map((item) => String(item))),
4896
+ sort: normalizeSortForFingerprint(params.sort),
4897
+ filters: params.filters,
4898
+ select_columns: params.select_columns.map((item) => String(item.que_id)),
4899
+ amount_column: params.amount_column ? String(params.amount_column.que_id) : null,
4900
+ time_range: params.time_column
4901
+ ? {
4902
+ column: String(params.time_column.que_id),
4903
+ from: params.time_range?.from ?? null,
4904
+ to: params.time_range?.to ?? null,
4905
+ timezone: params.time_range?.timezone ?? null
4906
+ }
4907
+ : null,
4908
+ stat_policy: params.stat_policy,
4909
+ row_cap: params.row_cap
4910
+ });
4911
+ }
4912
+ function buildAggregateContinuationFingerprint(params) {
4913
+ return buildQueryFingerprint({
4914
+ kind: "aggregate",
4915
+ app_key: params.app_key,
4916
+ mode: params.mode ?? null,
4917
+ type: params.type ?? null,
4918
+ keyword: params.keyword ?? null,
4919
+ query_logic: params.query_logic ?? null,
4920
+ apply_ids: uniqueStringList((params.apply_ids ?? []).map((item) => String(item))),
4921
+ sort: normalizeSortForFingerprint(params.sort),
4922
+ filters: params.filters,
4923
+ group_by: params.group_by.map((item) => String(item.que_id)),
4924
+ amount_columns: params.amount_columns.map((item) => String(item.que_id)),
4925
+ metrics: params.metrics,
4926
+ time_range: params.time_column
4927
+ ? {
4928
+ column: String(params.time_column.que_id),
4929
+ from: params.time_range?.from ?? null,
4930
+ to: params.time_range?.to ?? null,
4931
+ timezone: params.time_range?.timezone ?? null
4932
+ }
4933
+ : null,
4934
+ time_bucket: params.time_bucket,
4935
+ stat_policy: params.stat_policy
4936
+ });
4937
+ }
4330
4938
  async function executeRecordsSummary(args) {
4331
4939
  if (!args.app_key) {
4332
4940
  throw missingRequiredFieldError({
@@ -4343,13 +4951,13 @@ async function executeRecordsSummary(args) {
4343
4951
  });
4344
4952
  }
4345
4953
  const outputProfile = resolveOutputProfile(args.output_profile);
4346
- const queryId = randomUUID();
4347
4954
  const strictFull = args.strict_full ?? true;
4348
4955
  const includeNegative = args.stat_policy?.include_negative ?? true;
4349
4956
  const includeNull = args.stat_policy?.include_null ?? false;
4350
4957
  const scanMaxPages = args.scan_max_pages ?? DEFAULT_SCAN_MAX_PAGES;
4351
4958
  const requestedPages = args.requested_pages ?? scanMaxPages;
4352
- const startPage = resolveStartPage(args.page_num, args.page_token, args.app_key);
4959
+ const continuationPayload = resolveContinuationPayload(args.page_token, args.app_key);
4960
+ const startPage = continuationPayload?.next_page_num ?? args.page_num ?? 1;
4353
4961
  const pageSize = args.page_size ?? DEFAULT_PAGE_SIZE;
4354
4962
  const adaptivePaging = createAdaptivePagingState(pageSize);
4355
4963
  const rowCap = Math.min(args.max_rows ?? DEFAULT_ROW_LIMIT, DEFAULT_ROW_LIMIT);
@@ -4375,6 +4983,27 @@ async function executeRecordsSummary(args) {
4375
4983
  });
4376
4984
  }
4377
4985
  validateDateRangeFilters(summaryFilters, index, "qf_query(summary)");
4986
+ const queryFingerprint = buildSummaryContinuationFingerprint({
4987
+ app_key: args.app_key,
4988
+ mode: args.mode,
4989
+ type: args.type,
4990
+ keyword: args.keyword,
4991
+ query_logic: args.query_logic,
4992
+ apply_ids: args.apply_ids,
4993
+ sort: normalizedSort,
4994
+ filters: echoFilters(summaryFilters),
4995
+ select_columns: effectiveColumns,
4996
+ amount_column: amountColumn,
4997
+ time_column: timeColumn,
4998
+ time_range: args.time_range,
4999
+ stat_policy: {
5000
+ include_negative: includeNegative,
5001
+ include_null: includeNull
5002
+ },
5003
+ row_cap: rowCap
5004
+ });
5005
+ const resumed = loadContinuationState("summary", continuationPayload, queryFingerprint, "qf_query(summary)");
5006
+ const queryId = resumed?.state.query_id ?? randomUUID();
4378
5007
  const listState = {
4379
5008
  query_id: queryId,
4380
5009
  app_key: args.app_key,
@@ -4391,20 +5020,23 @@ async function executeRecordsSummary(args) {
4391
5020
  };
4392
5021
  let currentPage = startPage;
4393
5022
  const startedAt = Date.now();
4394
- let scannedPages = 0;
4395
- let scannedRecords = 0;
5023
+ const callScanLimit = resolveScanLimit(requestedPages, scanMaxPages);
5024
+ let scannedPagesThisCall = 0;
5025
+ let scannedPagesTotal = resumed?.state.source_pages.length ?? 0;
5026
+ let scannedRecords = resumed?.state.scanned_records ?? 0;
4396
5027
  let hasMore = false;
4397
5028
  let nextPageNum = null;
4398
5029
  let resultAmount = null;
4399
5030
  let summaryMeta = null;
4400
5031
  let stopReason = null;
4401
- let totalAmount = 0;
4402
- let missingCount = 0;
4403
- const sourcePages = [];
4404
- const rows = [];
4405
- const byDay = new Map();
4406
- while (scannedPages < requestedPages && scannedPages < scanMaxPages) {
4407
- if (scannedPages > 0 && isExecutionBudgetExceeded(startedAt)) {
5032
+ let totalAmount = resumed?.state.total_amount ?? 0;
5033
+ let missingCount = resumed?.state.missing_count ?? 0;
5034
+ const sourcePages = resumed ? [...resumed.state.source_pages] : [];
5035
+ const totalScanLimit = (resumed?.state.scan_limit_total ?? 0) + callScanLimit;
5036
+ const rows = resumed ? [...resumed.state.rows] : [];
5037
+ const byDay = new Map(resumed ? cloneByDayBuckets(resumed.state.by_day) : []);
5038
+ while (scannedPagesThisCall < callScanLimit) {
5039
+ if (scannedPagesThisCall > 0 && isExecutionBudgetExceeded(startedAt)) {
4408
5040
  hasMore = true;
4409
5041
  nextPageNum = currentPage;
4410
5042
  stopReason = "execution_budget";
@@ -4426,7 +5058,8 @@ async function executeRecordsSummary(args) {
4426
5058
  const response = await client.listRecords(args.app_key, payload, { userId: args.user_id });
4427
5059
  const fetchMs = Date.now() - fetchStartedAt;
4428
5060
  summaryMeta = summaryMeta ?? buildMeta(response);
4429
- scannedPages += 1;
5061
+ scannedPagesThisCall += 1;
5062
+ scannedPagesTotal += 1;
4430
5063
  sourcePages.push(currentPage);
4431
5064
  const result = asObject(response.result);
4432
5065
  const rawItems = asArray(result?.result);
@@ -4474,8 +5107,8 @@ async function executeRecordsSummary(args) {
4474
5107
  }
4475
5108
  const adaptiveDecision = applyAdaptivePaging({
4476
5109
  state: adaptivePaging,
4477
- fetchedPages: scannedPages,
4478
- requestedPages,
5110
+ fetchedPages: scannedPagesThisCall,
5111
+ requestedPages: callScanLimit,
4479
5112
  fetchMs,
4480
5113
  startedAt
4481
5114
  });
@@ -4533,33 +5166,49 @@ async function executeRecordsSummary(args) {
4533
5166
  }
4534
5167
  const knownResultAmount = resultAmount ?? scannedRecords;
4535
5168
  const omittedSourceItems = Math.max(0, knownResultAmount - scannedRecords);
4536
- const rawNextPageToken = hasMore && nextPageNum
4537
- ? encodeContinuationToken({
5169
+ let rawNextPageToken = null;
5170
+ const rawScanComplete = !hasMore && omittedSourceItems === 0;
5171
+ if (!rawScanComplete && nextPageNum) {
5172
+ const resumeId = setContinuationState("summary", {
5173
+ query_id: queryId,
5174
+ query_fingerprint: queryFingerprint,
5175
+ scanned_records: scannedRecords,
5176
+ total_amount: totalAmount,
5177
+ missing_count: missingCount,
5178
+ by_day: cloneByDayBuckets(Array.from(byDay.entries())),
5179
+ rows: [...rows],
5180
+ source_pages: [...sourcePages],
5181
+ scan_limit_total: totalScanLimit
5182
+ }, resumed?.resumeId);
5183
+ rawNextPageToken = encodeContinuationToken({
4538
5184
  app_key: args.app_key,
4539
5185
  next_page_num: nextPageNum,
4540
- page_size: adaptivePaging.current_page_size
4541
- })
4542
- : null;
4543
- const rawScanComplete = !hasMore && omittedSourceItems === 0;
5186
+ page_size: adaptivePaging.current_page_size,
5187
+ resume_kind: "summary",
5188
+ resume_id: resumeId
5189
+ });
5190
+ }
5191
+ else {
5192
+ deleteContinuationState(resumed?.resumeId);
5193
+ }
4544
5194
  const outputPageComplete = rows.length >= scannedRecords;
4545
- const scanLimit = resolveScanLimit(requestedPages, scanMaxPages);
4546
5195
  const scanLimitHit = !rawScanComplete &&
4547
- (scannedPages >= scanLimit ||
5196
+ (scannedPagesThisCall >= callScanLimit ||
4548
5197
  stopReason === "execution_budget" ||
4549
5198
  stopReason === "adaptive_budget");
4550
5199
  const completeness = buildExtendedCompleteness({
4551
5200
  resultAmount: knownResultAmount,
4552
5201
  returnedItems: scannedRecords,
4553
- fetchedPages: scannedPages,
4554
- requestedPages,
5202
+ fetchedPages: scannedPagesTotal,
5203
+ requestedPages: totalScanLimit,
4555
5204
  hasMore,
4556
5205
  nextPageToken: rawNextPageToken,
4557
5206
  omittedItems: omittedSourceItems,
4558
5207
  omittedChars: 0,
4559
5208
  rawScanComplete,
4560
5209
  scanLimitHit,
4561
- scannedPages,
4562
- scanLimit,
5210
+ scannedPages: scannedPagesTotal,
5211
+ scanLimit: totalScanLimit,
4563
5212
  outputPageComplete,
4564
5213
  rawNextPageToken,
4565
5214
  outputNextPageToken: null,
@@ -4605,11 +5254,11 @@ async function executeRecordsSummary(args) {
4605
5254
  },
4606
5255
  execution: {
4607
5256
  scanned_records: scannedRecords,
4608
- scanned_pages: scannedPages,
5257
+ scanned_pages: scannedPagesTotal,
4609
5258
  truncated: !completeness.is_complete,
4610
5259
  row_cap: rowCap,
4611
5260
  column_cap: args.max_columns ?? null,
4612
- scan_max_pages: scanMaxPages
5261
+ scan_max_pages: totalScanLimit
4613
5262
  }
4614
5263
  }
4615
5264
  }
@@ -4625,7 +5274,6 @@ async function executeRecordsSummary(args) {
4625
5274
  };
4626
5275
  }
4627
5276
  async function executeRecordsAggregate(args) {
4628
- const queryId = randomUUID();
4629
5277
  const strictFull = args.strict_full ?? true;
4630
5278
  const outputProfile = resolveOutputProfile(args.output_profile);
4631
5279
  const includeNegative = args.stat_policy?.include_negative ?? true;
@@ -4634,7 +5282,8 @@ async function executeRecordsAggregate(args) {
4634
5282
  const adaptivePaging = createAdaptivePagingState(pageSize);
4635
5283
  const scanMaxPages = args.scan_max_pages ?? DEFAULT_SCAN_MAX_PAGES;
4636
5284
  const requestedPages = args.requested_pages ?? scanMaxPages;
4637
- const startPage = resolveStartPage(args.page_num, args.page_token, args.app_key);
5285
+ const continuationPayload = resolveContinuationPayload(args.page_token, args.app_key);
5286
+ const startPage = continuationPayload?.next_page_num ?? args.page_num ?? 1;
4638
5287
  const maxGroups = args.max_groups ?? 200;
4639
5288
  const timezone = args.time_range?.timezone ?? "Asia/Shanghai";
4640
5289
  const timeBucket = args.time_bucket ?? null;
@@ -4662,6 +5311,28 @@ async function executeRecordsAggregate(args) {
4662
5311
  });
4663
5312
  }
4664
5313
  validateDateRangeFilters(aggregateFilters, index, "qf_records_aggregate");
5314
+ const queryFingerprint = buildAggregateContinuationFingerprint({
5315
+ app_key: args.app_key,
5316
+ mode: args.mode,
5317
+ type: args.type,
5318
+ keyword: args.keyword,
5319
+ query_logic: args.query_logic,
5320
+ apply_ids: args.apply_ids,
5321
+ sort: normalizedSort,
5322
+ filters: echoFilters(aggregateFilters),
5323
+ group_by: groupColumns,
5324
+ amount_columns: amountColumns,
5325
+ metrics,
5326
+ time_column: timeColumn,
5327
+ time_range: args.time_range,
5328
+ time_bucket: timeBucket,
5329
+ stat_policy: {
5330
+ include_negative: includeNegative,
5331
+ include_null: includeNull
5332
+ }
5333
+ });
5334
+ const resumed = loadContinuationState("aggregate", continuationPayload, queryFingerprint, "qf_records_aggregate");
5335
+ const queryId = resumed?.state.query_id ?? randomUUID();
4665
5336
  const listState = {
4666
5337
  query_id: queryId,
4667
5338
  app_key: args.app_key,
@@ -4682,19 +5353,24 @@ async function executeRecordsAggregate(args) {
4682
5353
  };
4683
5354
  let currentPage = startPage;
4684
5355
  const startedAt = Date.now();
4685
- let scannedPages = 0;
4686
- let scannedRecords = 0;
5356
+ const callScanLimit = resolveScanLimit(requestedPages, scanMaxPages);
5357
+ let scannedPagesThisCall = 0;
5358
+ let scannedPagesTotal = resumed?.state.source_pages.length ?? 0;
5359
+ let scannedRecords = resumed?.state.scanned_records ?? 0;
4687
5360
  let hasMore = false;
4688
5361
  let nextPageNum = null;
4689
5362
  let resultAmount = null;
4690
5363
  let responseMeta = null;
4691
5364
  let stopReason = null;
4692
- let totalAmount = 0;
4693
- const sourcePages = [];
4694
- const groupStats = new Map();
4695
- const summaryMetricStats = new Map();
4696
- while (scannedPages < requestedPages && scannedPages < scanMaxPages) {
4697
- if (scannedPages > 0 && isExecutionBudgetExceeded(startedAt)) {
5365
+ let totalAmount = resumed?.state.total_amount ?? 0;
5366
+ const sourcePages = resumed ? [...resumed.state.source_pages] : [];
5367
+ const totalScanLimit = (resumed?.state.scan_limit_total ?? 0) + callScanLimit;
5368
+ const groupStats = resumed ? restoreAggregateGroupStats(resumed.state.group_stats) : new Map();
5369
+ const summaryMetricStats = resumed
5370
+ ? restoreMetricAccumulatorMap(resumed.state.summary_metric_stats)
5371
+ : new Map();
5372
+ while (scannedPagesThisCall < callScanLimit) {
5373
+ if (scannedPagesThisCall > 0 && isExecutionBudgetExceeded(startedAt)) {
4698
5374
  hasMore = true;
4699
5375
  nextPageNum = currentPage;
4700
5376
  stopReason = "execution_budget";
@@ -4716,7 +5392,8 @@ async function executeRecordsAggregate(args) {
4716
5392
  const response = await client.listRecords(args.app_key, payload, { userId: args.user_id });
4717
5393
  const fetchMs = Date.now() - fetchStartedAt;
4718
5394
  responseMeta = responseMeta ?? buildMeta(response);
4719
- scannedPages += 1;
5395
+ scannedPagesThisCall += 1;
5396
+ scannedPagesTotal += 1;
4720
5397
  sourcePages.push(currentPage);
4721
5398
  const result = asObject(response.result);
4722
5399
  const rawItems = asArray(result?.result);
@@ -4770,8 +5447,8 @@ async function executeRecordsAggregate(args) {
4770
5447
  }
4771
5448
  const adaptiveDecision = applyAdaptivePaging({
4772
5449
  state: adaptivePaging,
4773
- fetchedPages: scannedPages,
4774
- requestedPages,
5450
+ fetchedPages: scannedPagesThisCall,
5451
+ requestedPages: callScanLimit,
4775
5452
  fetchMs,
4776
5453
  startedAt
4777
5454
  });
@@ -4791,34 +5468,49 @@ async function executeRecordsAggregate(args) {
4791
5468
  }
4792
5469
  const knownResultAmount = resultAmount ?? scannedRecords;
4793
5470
  const omittedSourceItems = Math.max(0, knownResultAmount - scannedRecords);
4794
- const rawNextPageToken = hasMore && nextPageNum
4795
- ? encodeContinuationToken({
4796
- app_key: args.app_key,
4797
- next_page_num: nextPageNum,
4798
- page_size: adaptivePaging.current_page_size
4799
- })
4800
- : null;
5471
+ let rawNextPageToken = null;
4801
5472
  const groupsTotal = groupStats.size;
4802
5473
  const rawScanComplete = !hasMore && omittedSourceItems === 0;
5474
+ if (!rawScanComplete && nextPageNum) {
5475
+ const resumeId = setContinuationState("aggregate", {
5476
+ query_id: queryId,
5477
+ query_fingerprint: queryFingerprint,
5478
+ scanned_records: scannedRecords,
5479
+ total_amount: totalAmount,
5480
+ source_pages: [...sourcePages],
5481
+ scan_limit_total: totalScanLimit,
5482
+ group_stats: serializeAggregateGroupStats(groupStats),
5483
+ summary_metric_stats: serializeMetricAccumulatorMap(summaryMetricStats)
5484
+ }, resumed?.resumeId);
5485
+ rawNextPageToken = encodeContinuationToken({
5486
+ app_key: args.app_key,
5487
+ next_page_num: nextPageNum,
5488
+ page_size: adaptivePaging.current_page_size,
5489
+ resume_kind: "aggregate",
5490
+ resume_id: resumeId
5491
+ });
5492
+ }
5493
+ else {
5494
+ deleteContinuationState(resumed?.resumeId);
5495
+ }
4803
5496
  const outputPageComplete = groupsTotal <= maxGroups;
4804
- const scanLimit = resolveScanLimit(requestedPages, scanMaxPages);
4805
5497
  const scanLimitHit = !rawScanComplete &&
4806
- (scannedPages >= scanLimit ||
5498
+ (scannedPagesThisCall >= callScanLimit ||
4807
5499
  stopReason === "execution_budget" ||
4808
5500
  stopReason === "adaptive_budget");
4809
5501
  const completeness = buildExtendedCompleteness({
4810
5502
  resultAmount: knownResultAmount,
4811
5503
  returnedItems: scannedRecords,
4812
- fetchedPages: scannedPages,
4813
- requestedPages,
5504
+ fetchedPages: scannedPagesTotal,
5505
+ requestedPages: totalScanLimit,
4814
5506
  hasMore,
4815
5507
  nextPageToken: rawNextPageToken,
4816
5508
  omittedItems: omittedSourceItems,
4817
5509
  omittedChars: 0,
4818
5510
  rawScanComplete,
4819
5511
  scanLimitHit,
4820
- scannedPages,
4821
- scanLimit,
5512
+ scannedPages: scannedPagesTotal,
5513
+ scanLimit: totalScanLimit,
4822
5514
  outputPageComplete,
4823
5515
  rawNextPageToken,
4824
5516
  outputNextPageToken: null,