qingflow-mcp 0.2.4 → 0.2.6
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.
- package/README.md +34 -0
- package/dist/server.js +708 -101
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ This MCP server wraps Qingflow OpenAPI for:
|
|
|
6
6
|
- `qf_form_get`
|
|
7
7
|
- `qf_records_list`
|
|
8
8
|
- `qf_record_get`
|
|
9
|
+
- `qf_query` (unified read entry: list / record / summary)
|
|
9
10
|
- `qf_record_create`
|
|
10
11
|
- `qf_record_update`
|
|
11
12
|
- `qf_operation_get`
|
|
@@ -52,6 +53,12 @@ npm run build
|
|
|
52
53
|
npm start
|
|
53
54
|
```
|
|
54
55
|
|
|
56
|
+
Run tests:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm test
|
|
60
|
+
```
|
|
61
|
+
|
|
55
62
|
## CLI Install
|
|
56
63
|
|
|
57
64
|
Global install from GitHub:
|
|
@@ -103,8 +110,35 @@ MCP client config example:
|
|
|
103
110
|
3. `qf_record_create` or `qf_record_update`.
|
|
104
111
|
4. If create/update returns only `request_id`, call `qf_operation_get` to resolve async result.
|
|
105
112
|
|
|
113
|
+
## Unified Query (`qf_query`)
|
|
114
|
+
|
|
115
|
+
`qf_query` is the recommended read entry for agents.
|
|
116
|
+
|
|
117
|
+
1. `query_mode=auto`:
|
|
118
|
+
- if `apply_id` is set, route to single-record query.
|
|
119
|
+
- if summary params are set (`amount_column` / `time_range` / `stat_policy` / `scan_max_pages`), route to summary query.
|
|
120
|
+
- otherwise route to list query.
|
|
121
|
+
2. `query_mode=list|record|summary` forces explicit behavior.
|
|
122
|
+
|
|
123
|
+
Summary mode output:
|
|
124
|
+
|
|
125
|
+
1. `summary`: aggregated stats (`total_count`, `total_amount`, `by_day`, `missing_count`).
|
|
126
|
+
2. `rows`: strict column rows (only requested `select_columns`).
|
|
127
|
+
3. `meta`: field mapping, filter scope, stat policy, execution limits.
|
|
128
|
+
|
|
129
|
+
Return shape:
|
|
130
|
+
|
|
131
|
+
1. success: structured payload `{ "ok": true, "data": ..., "meta": ... }`
|
|
132
|
+
2. failure: MCP `isError=true`, and text content is JSON payload like `{ "ok": false, "message": ..., ... }`
|
|
133
|
+
|
|
106
134
|
## List Query Tips
|
|
107
135
|
|
|
136
|
+
Strict mode (`qf_records_list`):
|
|
137
|
+
|
|
138
|
+
1. `select_columns` is required.
|
|
139
|
+
2. `include_answers=false` is not allowed.
|
|
140
|
+
3. Output `items[].answers` contains only selected columns, not full answers.
|
|
141
|
+
|
|
108
142
|
1. For `qf_records_list.sort[].que_id`, use a real field `que_id` (numeric) or exact field title from `qf_form_get`.
|
|
109
143
|
2. Avoid aliases like `create_time`; Qingflow often rejects them.
|
|
110
144
|
3. Use `max_rows` (or `max_items`) to cap returned rows.
|
package/dist/server.js
CHANGED
|
@@ -36,7 +36,7 @@ const client = new QingflowClient({
|
|
|
36
36
|
});
|
|
37
37
|
const server = new McpServer({
|
|
38
38
|
name: "qingflow-mcp",
|
|
39
|
-
version: "0.2.
|
|
39
|
+
version: "0.2.6"
|
|
40
40
|
});
|
|
41
41
|
const jsonPrimitiveSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
42
42
|
const answerValueSchema = z.union([
|
|
@@ -120,7 +120,7 @@ const appsInputSchema = z.object({
|
|
|
120
120
|
limit: z.number().int().positive().max(500).optional(),
|
|
121
121
|
offset: z.number().int().nonnegative().optional()
|
|
122
122
|
});
|
|
123
|
-
const
|
|
123
|
+
const appsSuccessOutputSchema = z.object({
|
|
124
124
|
ok: z.literal(true),
|
|
125
125
|
data: z.object({
|
|
126
126
|
total_apps: z.number().int().nonnegative(),
|
|
@@ -131,13 +131,14 @@ const appsOutputSchema = z.object({
|
|
|
131
131
|
}),
|
|
132
132
|
meta: apiMetaSchema
|
|
133
133
|
});
|
|
134
|
+
const appsOutputSchema = appsSuccessOutputSchema;
|
|
134
135
|
const formInputSchema = z.object({
|
|
135
136
|
app_key: z.string().min(1),
|
|
136
137
|
user_id: z.string().min(1).optional(),
|
|
137
138
|
force_refresh: z.boolean().optional(),
|
|
138
139
|
include_raw: z.boolean().optional()
|
|
139
140
|
});
|
|
140
|
-
const
|
|
141
|
+
const formSuccessOutputSchema = z.object({
|
|
141
142
|
ok: z.literal(true),
|
|
142
143
|
data: z.object({
|
|
143
144
|
app_key: z.string(),
|
|
@@ -147,7 +148,9 @@ const formOutputSchema = z.object({
|
|
|
147
148
|
}),
|
|
148
149
|
meta: apiMetaSchema
|
|
149
150
|
});
|
|
150
|
-
const
|
|
151
|
+
const formOutputSchema = formSuccessOutputSchema;
|
|
152
|
+
const listInputSchema = z
|
|
153
|
+
.object({
|
|
151
154
|
app_key: z.string().min(1),
|
|
152
155
|
user_id: z.string().min(1).optional(),
|
|
153
156
|
page_num: z.number().int().positive().optional(),
|
|
@@ -193,14 +196,14 @@ const listInputSchema = z.object({
|
|
|
193
196
|
max_rows: z.number().int().positive().max(200).optional(),
|
|
194
197
|
max_items: z.number().int().positive().max(200).optional(),
|
|
195
198
|
max_columns: z.number().int().positive().max(200).optional(),
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
.min(1)
|
|
199
|
-
.max(200)
|
|
200
|
-
.optional(),
|
|
199
|
+
// Strict mode: callers must explicitly choose columns.
|
|
200
|
+
select_columns: z.array(z.union([z.string().min(1), z.number().int()])).min(1).max(200),
|
|
201
201
|
include_answers: z.boolean().optional()
|
|
202
|
+
})
|
|
203
|
+
.refine((value) => value.include_answers !== false, {
|
|
204
|
+
message: "include_answers=false is not allowed in strict column mode"
|
|
202
205
|
});
|
|
203
|
-
const
|
|
206
|
+
const listSuccessOutputSchema = z.object({
|
|
204
207
|
ok: z.literal(true),
|
|
205
208
|
data: z.object({
|
|
206
209
|
app_key: z.string(),
|
|
@@ -216,12 +219,13 @@ const listOutputSchema = z.object({
|
|
|
216
219
|
include_answers: z.boolean(),
|
|
217
220
|
row_cap: z.number().int().nonnegative(),
|
|
218
221
|
column_cap: z.number().int().positive().nullable(),
|
|
219
|
-
selected_columns: z.array(z.string())
|
|
222
|
+
selected_columns: z.array(z.string())
|
|
220
223
|
})
|
|
221
224
|
.optional()
|
|
222
225
|
}),
|
|
223
226
|
meta: apiMetaSchema
|
|
224
227
|
});
|
|
228
|
+
const listOutputSchema = listSuccessOutputSchema;
|
|
225
229
|
const recordGetInputSchema = z.object({
|
|
226
230
|
apply_id: z.union([z.string().min(1), z.number().int()]),
|
|
227
231
|
max_columns: z.number().int().positive().max(200).optional(),
|
|
@@ -231,7 +235,7 @@ const recordGetInputSchema = z.object({
|
|
|
231
235
|
.max(200)
|
|
232
236
|
.optional()
|
|
233
237
|
});
|
|
234
|
-
const
|
|
238
|
+
const recordGetSuccessOutputSchema = z.object({
|
|
235
239
|
ok: z.literal(true),
|
|
236
240
|
data: z.object({
|
|
237
241
|
apply_id: z.union([z.string(), z.number(), z.null()]),
|
|
@@ -246,6 +250,7 @@ const recordGetOutputSchema = z.object({
|
|
|
246
250
|
}),
|
|
247
251
|
meta: apiMetaSchema
|
|
248
252
|
});
|
|
253
|
+
const recordGetOutputSchema = recordGetSuccessOutputSchema;
|
|
249
254
|
const createInputSchema = z
|
|
250
255
|
.object({
|
|
251
256
|
app_key: z.string().min(1),
|
|
@@ -265,7 +270,7 @@ const createInputSchema = z
|
|
|
265
270
|
.refine((value) => hasWritePayload(value.answers, value.fields), {
|
|
266
271
|
message: "Either answers or fields is required"
|
|
267
272
|
});
|
|
268
|
-
const
|
|
273
|
+
const createSuccessOutputSchema = z.object({
|
|
269
274
|
ok: z.literal(true),
|
|
270
275
|
data: z.object({
|
|
271
276
|
request_id: z.string().nullable(),
|
|
@@ -274,6 +279,7 @@ const createOutputSchema = z.object({
|
|
|
274
279
|
}),
|
|
275
280
|
meta: apiMetaSchema
|
|
276
281
|
});
|
|
282
|
+
const createOutputSchema = createSuccessOutputSchema;
|
|
277
283
|
const updateInputSchema = z
|
|
278
284
|
.object({
|
|
279
285
|
apply_id: z.union([z.string().min(1), z.number().int()]),
|
|
@@ -286,7 +292,7 @@ const updateInputSchema = z
|
|
|
286
292
|
.refine((value) => hasWritePayload(value.answers, value.fields), {
|
|
287
293
|
message: "Either answers or fields is required"
|
|
288
294
|
});
|
|
289
|
-
const
|
|
295
|
+
const updateSuccessOutputSchema = z.object({
|
|
290
296
|
ok: z.literal(true),
|
|
291
297
|
data: z.object({
|
|
292
298
|
request_id: z.string().nullable(),
|
|
@@ -294,14 +300,144 @@ const updateOutputSchema = z.object({
|
|
|
294
300
|
}),
|
|
295
301
|
meta: apiMetaSchema
|
|
296
302
|
});
|
|
303
|
+
const updateOutputSchema = updateSuccessOutputSchema;
|
|
297
304
|
const operationInputSchema = z.object({
|
|
298
305
|
request_id: z.string().min(1)
|
|
299
306
|
});
|
|
300
|
-
const
|
|
307
|
+
const operationSuccessOutputSchema = z.object({
|
|
301
308
|
ok: z.literal(true),
|
|
302
309
|
data: operationResultSchema,
|
|
303
310
|
meta: apiMetaSchema
|
|
304
311
|
});
|
|
312
|
+
const operationOutputSchema = operationSuccessOutputSchema;
|
|
313
|
+
const queryInputSchema = z.object({
|
|
314
|
+
query_mode: z.enum(["auto", "list", "record", "summary"]).optional(),
|
|
315
|
+
app_key: z.string().min(1).optional(),
|
|
316
|
+
apply_id: z.union([z.string().min(1), z.number().int()]).optional(),
|
|
317
|
+
user_id: z.string().min(1).optional(),
|
|
318
|
+
page_num: z.number().int().positive().optional(),
|
|
319
|
+
page_size: z.number().int().positive().max(200).optional(),
|
|
320
|
+
mode: z
|
|
321
|
+
.enum([
|
|
322
|
+
"todo",
|
|
323
|
+
"done",
|
|
324
|
+
"mine_approved",
|
|
325
|
+
"mine_rejected",
|
|
326
|
+
"mine_draft",
|
|
327
|
+
"mine_need_improve",
|
|
328
|
+
"mine_processing",
|
|
329
|
+
"all",
|
|
330
|
+
"all_approved",
|
|
331
|
+
"all_rejected",
|
|
332
|
+
"all_processing",
|
|
333
|
+
"cc"
|
|
334
|
+
])
|
|
335
|
+
.optional(),
|
|
336
|
+
type: z.number().int().min(1).max(12).optional(),
|
|
337
|
+
keyword: z.string().optional(),
|
|
338
|
+
query_logic: z.enum(["and", "or"]).optional(),
|
|
339
|
+
apply_ids: z.array(z.union([z.string(), z.number()])).optional(),
|
|
340
|
+
sort: z
|
|
341
|
+
.array(z.object({
|
|
342
|
+
que_id: z.union([z.string().min(1), z.number().int()]),
|
|
343
|
+
ascend: z.boolean().optional()
|
|
344
|
+
}))
|
|
345
|
+
.optional(),
|
|
346
|
+
filters: z
|
|
347
|
+
.array(z.object({
|
|
348
|
+
que_id: z.union([z.string().min(1), z.number().int()]).optional(),
|
|
349
|
+
search_key: z.string().optional(),
|
|
350
|
+
search_keys: z.array(z.string()).optional(),
|
|
351
|
+
min_value: z.string().optional(),
|
|
352
|
+
max_value: z.string().optional(),
|
|
353
|
+
scope: z.number().int().optional(),
|
|
354
|
+
search_options: z.array(z.union([z.string(), z.number()])).optional(),
|
|
355
|
+
search_user_ids: z.array(z.string()).optional()
|
|
356
|
+
}))
|
|
357
|
+
.optional(),
|
|
358
|
+
max_rows: z.number().int().positive().max(200).optional(),
|
|
359
|
+
max_items: z.number().int().positive().max(200).optional(),
|
|
360
|
+
max_columns: z.number().int().positive().max(200).optional(),
|
|
361
|
+
select_columns: z
|
|
362
|
+
.array(z.union([z.string().min(1), z.number().int()]))
|
|
363
|
+
.min(1)
|
|
364
|
+
.max(200)
|
|
365
|
+
.optional(),
|
|
366
|
+
include_answers: z.boolean().optional(),
|
|
367
|
+
amount_column: z.union([z.string().min(1), z.number().int()]).optional(),
|
|
368
|
+
time_range: z
|
|
369
|
+
.object({
|
|
370
|
+
column: z.union([z.string().min(1), z.number().int()]),
|
|
371
|
+
from: z.string().optional(),
|
|
372
|
+
to: z.string().optional(),
|
|
373
|
+
timezone: z.string().optional()
|
|
374
|
+
})
|
|
375
|
+
.optional(),
|
|
376
|
+
stat_policy: z
|
|
377
|
+
.object({
|
|
378
|
+
include_negative: z.boolean().optional(),
|
|
379
|
+
include_null: z.boolean().optional()
|
|
380
|
+
})
|
|
381
|
+
.optional(),
|
|
382
|
+
scan_max_pages: z.number().int().positive().max(500).optional()
|
|
383
|
+
});
|
|
384
|
+
const querySummaryOutputSchema = z.object({
|
|
385
|
+
summary: z.object({
|
|
386
|
+
total_count: z.number().int().nonnegative(),
|
|
387
|
+
total_amount: z.number().nullable(),
|
|
388
|
+
by_day: z.array(z.object({
|
|
389
|
+
day: z.string(),
|
|
390
|
+
count: z.number().int().nonnegative(),
|
|
391
|
+
amount_total: z.number().nullable()
|
|
392
|
+
})),
|
|
393
|
+
missing_count: z.number().int().nonnegative()
|
|
394
|
+
}),
|
|
395
|
+
rows: z.array(z.record(z.unknown())),
|
|
396
|
+
meta: z.object({
|
|
397
|
+
field_mapping: z.array(z.object({
|
|
398
|
+
role: z.enum(["row", "amount", "time"]),
|
|
399
|
+
requested: z.string(),
|
|
400
|
+
que_id: z.union([z.string(), z.number()]),
|
|
401
|
+
que_title: z.string().nullable(),
|
|
402
|
+
que_type: z.unknown()
|
|
403
|
+
})),
|
|
404
|
+
filters: z.object({
|
|
405
|
+
app_key: z.string(),
|
|
406
|
+
time_range: z
|
|
407
|
+
.object({
|
|
408
|
+
column: z.string(),
|
|
409
|
+
from: z.string().nullable(),
|
|
410
|
+
to: z.string().nullable(),
|
|
411
|
+
timezone: z.string()
|
|
412
|
+
})
|
|
413
|
+
.nullable()
|
|
414
|
+
}),
|
|
415
|
+
stat_policy: z.object({
|
|
416
|
+
include_negative: z.boolean(),
|
|
417
|
+
include_null: z.boolean()
|
|
418
|
+
}),
|
|
419
|
+
execution: z.object({
|
|
420
|
+
scanned_records: z.number().int().nonnegative(),
|
|
421
|
+
scanned_pages: z.number().int().nonnegative(),
|
|
422
|
+
truncated: z.boolean(),
|
|
423
|
+
row_cap: z.number().int().positive(),
|
|
424
|
+
column_cap: z.number().int().positive().nullable(),
|
|
425
|
+
scan_max_pages: z.number().int().positive()
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
});
|
|
429
|
+
const querySuccessOutputSchema = z.object({
|
|
430
|
+
ok: z.literal(true),
|
|
431
|
+
data: z.object({
|
|
432
|
+
mode: z.enum(["list", "record", "summary"]),
|
|
433
|
+
source_tool: z.enum(["qf_records_list", "qf_record_get", "qf_records_summary"]),
|
|
434
|
+
list: listSuccessOutputSchema.shape.data.optional(),
|
|
435
|
+
record: recordGetSuccessOutputSchema.shape.data.optional(),
|
|
436
|
+
summary: querySummaryOutputSchema.optional()
|
|
437
|
+
}),
|
|
438
|
+
meta: apiMetaSchema
|
|
439
|
+
});
|
|
440
|
+
const queryOutputSchema = querySuccessOutputSchema;
|
|
305
441
|
server.registerTool("qf_apps_list", {
|
|
306
442
|
title: "Qingflow Apps List",
|
|
307
443
|
description: "List Qingflow apps with optional filtering and client-side slicing.",
|
|
@@ -389,68 +525,8 @@ server.registerTool("qf_records_list", {
|
|
|
389
525
|
}
|
|
390
526
|
}, async (args) => {
|
|
391
527
|
try {
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
|
|
395
|
-
const includeAnswers = Boolean(args.include_answers || (args.select_columns && args.select_columns.length > 0));
|
|
396
|
-
const payload = buildListPayload({
|
|
397
|
-
pageNum,
|
|
398
|
-
pageSize,
|
|
399
|
-
mode: args.mode,
|
|
400
|
-
type: args.type,
|
|
401
|
-
keyword: args.keyword,
|
|
402
|
-
queryLogic: args.query_logic,
|
|
403
|
-
applyIds: args.apply_ids,
|
|
404
|
-
sort: normalizedSort,
|
|
405
|
-
filters: args.filters
|
|
406
|
-
});
|
|
407
|
-
const response = await client.listRecords(args.app_key, payload, { userId: args.user_id });
|
|
408
|
-
const result = asObject(response.result);
|
|
409
|
-
const rawItems = asArray(result?.result);
|
|
410
|
-
const listLimit = resolveListItemLimit({
|
|
411
|
-
total: rawItems.length,
|
|
412
|
-
requestedMaxRows: args.max_rows,
|
|
413
|
-
requestedMaxItems: args.max_items,
|
|
414
|
-
includeAnswers
|
|
415
|
-
});
|
|
416
|
-
const items = rawItems
|
|
417
|
-
.slice(0, listLimit.limit)
|
|
418
|
-
.map((raw) => normalizeRecordItem(raw, includeAnswers));
|
|
419
|
-
const columnProjection = projectRecordItemsColumns({
|
|
420
|
-
items,
|
|
421
|
-
includeAnswers,
|
|
422
|
-
maxColumns: args.max_columns,
|
|
423
|
-
selectColumns: args.select_columns
|
|
424
|
-
});
|
|
425
|
-
const fitted = fitListItemsWithinSize({
|
|
426
|
-
items: columnProjection.items,
|
|
427
|
-
limitBytes: MAX_LIST_ITEMS_BYTES
|
|
428
|
-
});
|
|
429
|
-
const truncationReason = mergeTruncationReasons(listLimit.reason, columnProjection.reason, fitted.reason);
|
|
430
|
-
return okResult({
|
|
431
|
-
ok: true,
|
|
432
|
-
data: {
|
|
433
|
-
app_key: args.app_key,
|
|
434
|
-
pagination: {
|
|
435
|
-
page_num: toPositiveInt(result?.pageNum) ?? pageNum,
|
|
436
|
-
page_size: toPositiveInt(result?.pageSize) ?? pageSize,
|
|
437
|
-
page_amount: toNonNegativeInt(result?.pageAmount),
|
|
438
|
-
result_amount: toNonNegativeInt(result?.resultAmount) ?? fitted.items.length
|
|
439
|
-
},
|
|
440
|
-
items: fitted.items,
|
|
441
|
-
applied_limits: {
|
|
442
|
-
include_answers: includeAnswers,
|
|
443
|
-
row_cap: listLimit.limit,
|
|
444
|
-
column_cap: args.max_columns ?? null,
|
|
445
|
-
selected_columns: columnProjection.selectedColumns
|
|
446
|
-
}
|
|
447
|
-
},
|
|
448
|
-
meta: buildMeta(response)
|
|
449
|
-
}, buildRecordsListMessage({
|
|
450
|
-
returned: fitted.items.length,
|
|
451
|
-
total: rawItems.length,
|
|
452
|
-
truncationReason
|
|
453
|
-
}));
|
|
528
|
+
const executed = await executeRecordsList(args);
|
|
529
|
+
return okResult(executed.payload, executed.message);
|
|
454
530
|
}
|
|
455
531
|
catch (error) {
|
|
456
532
|
return errorResult(error);
|
|
@@ -467,31 +543,61 @@ server.registerTool("qf_record_get", {
|
|
|
467
543
|
}
|
|
468
544
|
}, async (args) => {
|
|
469
545
|
try {
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
546
|
+
const executed = await executeRecordGet(args);
|
|
547
|
+
return okResult(executed.payload, executed.message);
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
return errorResult(error);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
server.registerTool("qf_query", {
|
|
554
|
+
title: "Qingflow Unified Query",
|
|
555
|
+
description: "Unified read entry for list/record/summary. Use query_mode=auto to route automatically.",
|
|
556
|
+
inputSchema: queryInputSchema,
|
|
557
|
+
outputSchema: queryOutputSchema,
|
|
558
|
+
annotations: {
|
|
559
|
+
readOnlyHint: true,
|
|
560
|
+
idempotentHint: true
|
|
561
|
+
}
|
|
562
|
+
}, async (args) => {
|
|
563
|
+
try {
|
|
564
|
+
const routedMode = resolveQueryMode(args);
|
|
565
|
+
if (routedMode === "record") {
|
|
566
|
+
const recordArgs = buildRecordGetArgsFromQuery(args);
|
|
567
|
+
const executed = await executeRecordGet(recordArgs);
|
|
568
|
+
return okResult({
|
|
569
|
+
ok: true,
|
|
570
|
+
data: {
|
|
571
|
+
mode: "record",
|
|
572
|
+
source_tool: "qf_record_get",
|
|
573
|
+
record: executed.payload.data
|
|
574
|
+
},
|
|
575
|
+
meta: executed.payload.meta
|
|
576
|
+
}, executed.message);
|
|
577
|
+
}
|
|
578
|
+
if (routedMode === "summary") {
|
|
579
|
+
const executed = await executeRecordsSummary(args);
|
|
580
|
+
return okResult({
|
|
581
|
+
ok: true,
|
|
582
|
+
data: {
|
|
583
|
+
mode: "summary",
|
|
584
|
+
source_tool: "qf_records_summary",
|
|
585
|
+
summary: executed.data
|
|
586
|
+
},
|
|
587
|
+
meta: executed.meta
|
|
588
|
+
}, executed.message);
|
|
589
|
+
}
|
|
590
|
+
const listArgs = buildListArgsFromQuery(args);
|
|
591
|
+
const executed = await executeRecordsList(listArgs);
|
|
482
592
|
return okResult({
|
|
483
593
|
ok: true,
|
|
484
594
|
data: {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
applied_limits: {
|
|
489
|
-
column_cap: args.max_columns ?? null,
|
|
490
|
-
selected_columns: projection.selectedColumns
|
|
491
|
-
}
|
|
595
|
+
mode: "list",
|
|
596
|
+
source_tool: "qf_records_list",
|
|
597
|
+
list: executed.payload.data
|
|
492
598
|
},
|
|
493
|
-
meta:
|
|
494
|
-
},
|
|
599
|
+
meta: executed.payload.meta
|
|
600
|
+
}, executed.message);
|
|
495
601
|
}
|
|
496
602
|
catch (error) {
|
|
497
603
|
return errorResult(error);
|
|
@@ -618,6 +724,500 @@ function buildMeta(response) {
|
|
|
618
724
|
base_url: baseUrl
|
|
619
725
|
};
|
|
620
726
|
}
|
|
727
|
+
function resolveQueryMode(args) {
|
|
728
|
+
const requested = args.query_mode ?? "auto";
|
|
729
|
+
if (requested !== "auto") {
|
|
730
|
+
return requested;
|
|
731
|
+
}
|
|
732
|
+
if (args.apply_id !== undefined) {
|
|
733
|
+
return "record";
|
|
734
|
+
}
|
|
735
|
+
if (args.amount_column !== undefined ||
|
|
736
|
+
args.time_range !== undefined ||
|
|
737
|
+
args.stat_policy !== undefined ||
|
|
738
|
+
args.scan_max_pages !== undefined) {
|
|
739
|
+
return "summary";
|
|
740
|
+
}
|
|
741
|
+
return "list";
|
|
742
|
+
}
|
|
743
|
+
function buildListArgsFromQuery(args) {
|
|
744
|
+
if (!args.app_key) {
|
|
745
|
+
throw new Error("app_key is required for list query");
|
|
746
|
+
}
|
|
747
|
+
if (!args.select_columns?.length) {
|
|
748
|
+
throw new Error("select_columns is required for list query");
|
|
749
|
+
}
|
|
750
|
+
return listInputSchema.parse({
|
|
751
|
+
app_key: args.app_key,
|
|
752
|
+
user_id: args.user_id,
|
|
753
|
+
page_num: args.page_num,
|
|
754
|
+
page_size: args.page_size,
|
|
755
|
+
mode: args.mode,
|
|
756
|
+
type: args.type,
|
|
757
|
+
keyword: args.keyword,
|
|
758
|
+
query_logic: args.query_logic,
|
|
759
|
+
apply_ids: args.apply_ids,
|
|
760
|
+
sort: args.sort,
|
|
761
|
+
filters: args.filters,
|
|
762
|
+
max_rows: args.max_rows,
|
|
763
|
+
max_items: args.max_items,
|
|
764
|
+
max_columns: args.max_columns,
|
|
765
|
+
select_columns: args.select_columns,
|
|
766
|
+
include_answers: args.include_answers
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
function buildRecordGetArgsFromQuery(args) {
|
|
770
|
+
if (args.apply_id === undefined) {
|
|
771
|
+
throw new Error("apply_id is required for record query");
|
|
772
|
+
}
|
|
773
|
+
return recordGetInputSchema.parse({
|
|
774
|
+
apply_id: args.apply_id,
|
|
775
|
+
max_columns: args.max_columns,
|
|
776
|
+
select_columns: args.select_columns
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
async function executeRecordsList(args) {
|
|
780
|
+
const pageNum = args.page_num ?? 1;
|
|
781
|
+
const pageSize = args.page_size ?? DEFAULT_PAGE_SIZE;
|
|
782
|
+
const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
|
|
783
|
+
const includeAnswers = true;
|
|
784
|
+
const payload = buildListPayload({
|
|
785
|
+
pageNum,
|
|
786
|
+
pageSize,
|
|
787
|
+
mode: args.mode,
|
|
788
|
+
type: args.type,
|
|
789
|
+
keyword: args.keyword,
|
|
790
|
+
queryLogic: args.query_logic,
|
|
791
|
+
applyIds: args.apply_ids,
|
|
792
|
+
sort: normalizedSort,
|
|
793
|
+
filters: args.filters
|
|
794
|
+
});
|
|
795
|
+
const response = await client.listRecords(args.app_key, payload, { userId: args.user_id });
|
|
796
|
+
const result = asObject(response.result);
|
|
797
|
+
const rawItems = asArray(result?.result);
|
|
798
|
+
const listLimit = resolveListItemLimit({
|
|
799
|
+
total: rawItems.length,
|
|
800
|
+
requestedMaxRows: args.max_rows,
|
|
801
|
+
requestedMaxItems: args.max_items,
|
|
802
|
+
includeAnswers
|
|
803
|
+
});
|
|
804
|
+
const items = rawItems
|
|
805
|
+
.slice(0, listLimit.limit)
|
|
806
|
+
.map((raw) => normalizeRecordItem(raw, includeAnswers));
|
|
807
|
+
const columnProjection = projectRecordItemsColumns({
|
|
808
|
+
items,
|
|
809
|
+
includeAnswers,
|
|
810
|
+
maxColumns: args.max_columns,
|
|
811
|
+
selectColumns: args.select_columns
|
|
812
|
+
});
|
|
813
|
+
if (items.length > 0 && columnProjection.matchedAnswersCount === 0) {
|
|
814
|
+
throw new Error(`No answers matched select_columns (${args.select_columns
|
|
815
|
+
.map((item) => String(item))
|
|
816
|
+
.join(", ")}). Check que_id/title from qf_form_get.`);
|
|
817
|
+
}
|
|
818
|
+
const fitted = fitListItemsWithinSize({
|
|
819
|
+
items: columnProjection.items,
|
|
820
|
+
limitBytes: MAX_LIST_ITEMS_BYTES
|
|
821
|
+
});
|
|
822
|
+
const truncationReason = mergeTruncationReasons(listLimit.reason, columnProjection.reason, fitted.reason);
|
|
823
|
+
const responsePayload = {
|
|
824
|
+
ok: true,
|
|
825
|
+
data: {
|
|
826
|
+
app_key: args.app_key,
|
|
827
|
+
pagination: {
|
|
828
|
+
page_num: toPositiveInt(result?.pageNum) ?? pageNum,
|
|
829
|
+
page_size: toPositiveInt(result?.pageSize) ?? pageSize,
|
|
830
|
+
page_amount: toNonNegativeInt(result?.pageAmount),
|
|
831
|
+
result_amount: toNonNegativeInt(result?.resultAmount) ?? fitted.items.length
|
|
832
|
+
},
|
|
833
|
+
items: fitted.items,
|
|
834
|
+
applied_limits: {
|
|
835
|
+
include_answers: includeAnswers,
|
|
836
|
+
row_cap: listLimit.limit,
|
|
837
|
+
column_cap: args.max_columns ?? null,
|
|
838
|
+
selected_columns: columnProjection.selectedColumns
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
meta: buildMeta(response)
|
|
842
|
+
};
|
|
843
|
+
return {
|
|
844
|
+
payload: responsePayload,
|
|
845
|
+
message: buildRecordsListMessage({
|
|
846
|
+
returned: fitted.items.length,
|
|
847
|
+
total: rawItems.length,
|
|
848
|
+
truncationReason
|
|
849
|
+
})
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
async function executeRecordGet(args) {
|
|
853
|
+
const response = await client.getRecord(String(args.apply_id));
|
|
854
|
+
const record = asObject(response.result) ?? {};
|
|
855
|
+
const projection = projectAnswersForOutput({
|
|
856
|
+
answers: asArray(record.answers),
|
|
857
|
+
maxColumns: args.max_columns,
|
|
858
|
+
selectColumns: args.select_columns
|
|
859
|
+
});
|
|
860
|
+
const projectedRecord = {
|
|
861
|
+
...record,
|
|
862
|
+
answers: projection.answers
|
|
863
|
+
};
|
|
864
|
+
const answerCount = projection.answers.length;
|
|
865
|
+
return {
|
|
866
|
+
payload: {
|
|
867
|
+
ok: true,
|
|
868
|
+
data: {
|
|
869
|
+
apply_id: record.applyId ?? null,
|
|
870
|
+
answer_count: answerCount,
|
|
871
|
+
record: projectedRecord,
|
|
872
|
+
applied_limits: {
|
|
873
|
+
column_cap: args.max_columns ?? null,
|
|
874
|
+
selected_columns: projection.selectedColumns
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
meta: buildMeta(response)
|
|
878
|
+
},
|
|
879
|
+
message: `Fetched record ${String(args.apply_id)}`
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
async function executeRecordsSummary(args) {
|
|
883
|
+
if (!args.app_key) {
|
|
884
|
+
throw new Error("app_key is required for summary query");
|
|
885
|
+
}
|
|
886
|
+
if (!args.select_columns?.length) {
|
|
887
|
+
throw new Error("select_columns is required for summary query");
|
|
888
|
+
}
|
|
889
|
+
const includeNegative = args.stat_policy?.include_negative ?? true;
|
|
890
|
+
const includeNull = args.stat_policy?.include_null ?? false;
|
|
891
|
+
const scanMaxPages = args.scan_max_pages ?? 50;
|
|
892
|
+
const pageSize = args.page_size ?? DEFAULT_PAGE_SIZE;
|
|
893
|
+
const rowCap = Math.min(args.max_rows ?? DEFAULT_PAGE_SIZE, 200);
|
|
894
|
+
const timezone = args.time_range?.timezone ?? "Asia/Shanghai";
|
|
895
|
+
const form = await getFormCached(args.app_key, args.user_id, false);
|
|
896
|
+
const index = buildFieldIndex(form.result);
|
|
897
|
+
const selectedColumns = resolveSummaryColumns(args.select_columns, index, "select_columns");
|
|
898
|
+
const effectiveColumns = args.max_columns !== undefined ? selectedColumns.slice(0, args.max_columns) : selectedColumns;
|
|
899
|
+
if (effectiveColumns.length === 0) {
|
|
900
|
+
throw new Error("No output columns remain after max_columns cap");
|
|
901
|
+
}
|
|
902
|
+
const amountColumn = args.amount_column !== undefined
|
|
903
|
+
? resolveSummaryColumn(args.amount_column, index, "amount_column")
|
|
904
|
+
: null;
|
|
905
|
+
const timeColumn = args.time_range ? resolveSummaryColumn(args.time_range.column, index, "time_range.column") : null;
|
|
906
|
+
const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
|
|
907
|
+
const summaryFilters = [...(args.filters ?? [])];
|
|
908
|
+
if (timeColumn && (args.time_range?.from || args.time_range?.to)) {
|
|
909
|
+
summaryFilters.push({
|
|
910
|
+
que_id: timeColumn.que_id,
|
|
911
|
+
...(args.time_range.from ? { min_value: args.time_range.from } : {}),
|
|
912
|
+
...(args.time_range.to ? { max_value: args.time_range.to } : {})
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
let currentPage = args.page_num ?? 1;
|
|
916
|
+
let scannedPages = 0;
|
|
917
|
+
let scannedRecords = 0;
|
|
918
|
+
let truncated = false;
|
|
919
|
+
let summaryMeta = null;
|
|
920
|
+
let totalAmount = 0;
|
|
921
|
+
let missingCount = 0;
|
|
922
|
+
const rows = [];
|
|
923
|
+
const byDay = new Map();
|
|
924
|
+
while (true) {
|
|
925
|
+
const payload = buildListPayload({
|
|
926
|
+
pageNum: currentPage,
|
|
927
|
+
pageSize,
|
|
928
|
+
mode: args.mode,
|
|
929
|
+
type: args.type,
|
|
930
|
+
keyword: args.keyword,
|
|
931
|
+
queryLogic: args.query_logic,
|
|
932
|
+
applyIds: args.apply_ids,
|
|
933
|
+
sort: normalizedSort,
|
|
934
|
+
filters: summaryFilters
|
|
935
|
+
});
|
|
936
|
+
const response = await client.listRecords(args.app_key, payload, { userId: args.user_id });
|
|
937
|
+
summaryMeta = summaryMeta ?? buildMeta(response);
|
|
938
|
+
scannedPages += 1;
|
|
939
|
+
const result = asObject(response.result);
|
|
940
|
+
const rawItems = asArray(result?.result);
|
|
941
|
+
const pageAmount = toPositiveInt(result?.pageAmount);
|
|
942
|
+
const hasMoreByAmount = pageAmount !== null ? currentPage < pageAmount : rawItems.length === pageSize;
|
|
943
|
+
for (const rawItem of rawItems) {
|
|
944
|
+
const record = asObject(rawItem) ?? {};
|
|
945
|
+
const answers = asArray(record.answers);
|
|
946
|
+
scannedRecords += 1;
|
|
947
|
+
if (rows.length < rowCap) {
|
|
948
|
+
rows.push(buildSummaryRow(answers, effectiveColumns));
|
|
949
|
+
}
|
|
950
|
+
let amountContribution = 0;
|
|
951
|
+
let hasAmountContribution = false;
|
|
952
|
+
if (amountColumn) {
|
|
953
|
+
const amountValue = extractSummaryColumnValue(answers, amountColumn);
|
|
954
|
+
const numericAmount = toFiniteAmount(amountValue);
|
|
955
|
+
if (numericAmount === null) {
|
|
956
|
+
if (!includeNull) {
|
|
957
|
+
missingCount += 1;
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
hasAmountContribution = true;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
else if (includeNegative || numericAmount >= 0) {
|
|
964
|
+
amountContribution = numericAmount;
|
|
965
|
+
hasAmountContribution = true;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (hasAmountContribution) {
|
|
969
|
+
totalAmount += amountContribution;
|
|
970
|
+
}
|
|
971
|
+
const dayKey = timeColumn
|
|
972
|
+
? toDayBucket(extractSummaryColumnValue(answers, timeColumn), timezone)
|
|
973
|
+
: "all";
|
|
974
|
+
const bucket = byDay.get(dayKey) ?? { count: 0, amount: 0 };
|
|
975
|
+
bucket.count += 1;
|
|
976
|
+
if (amountColumn && hasAmountContribution) {
|
|
977
|
+
bucket.amount += amountContribution;
|
|
978
|
+
}
|
|
979
|
+
byDay.set(dayKey, bucket);
|
|
980
|
+
}
|
|
981
|
+
if (!hasMoreByAmount) {
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
if (scannedPages >= scanMaxPages) {
|
|
985
|
+
truncated = true;
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
currentPage += 1;
|
|
989
|
+
}
|
|
990
|
+
const byDayStats = Array.from(byDay.entries())
|
|
991
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
992
|
+
.map(([day, bucket]) => ({
|
|
993
|
+
day,
|
|
994
|
+
count: bucket.count,
|
|
995
|
+
amount_total: amountColumn ? bucket.amount : null
|
|
996
|
+
}));
|
|
997
|
+
const fieldMapping = [
|
|
998
|
+
...effectiveColumns.map((item) => ({
|
|
999
|
+
role: "row",
|
|
1000
|
+
requested: item.requested,
|
|
1001
|
+
que_id: item.que_id,
|
|
1002
|
+
que_title: item.que_title,
|
|
1003
|
+
que_type: item.que_type
|
|
1004
|
+
})),
|
|
1005
|
+
...(amountColumn
|
|
1006
|
+
? [
|
|
1007
|
+
{
|
|
1008
|
+
role: "amount",
|
|
1009
|
+
requested: amountColumn.requested,
|
|
1010
|
+
que_id: amountColumn.que_id,
|
|
1011
|
+
que_title: amountColumn.que_title,
|
|
1012
|
+
que_type: amountColumn.que_type
|
|
1013
|
+
}
|
|
1014
|
+
]
|
|
1015
|
+
: []),
|
|
1016
|
+
...(timeColumn
|
|
1017
|
+
? [
|
|
1018
|
+
{
|
|
1019
|
+
role: "time",
|
|
1020
|
+
requested: timeColumn.requested,
|
|
1021
|
+
que_id: timeColumn.que_id,
|
|
1022
|
+
que_title: timeColumn.que_title,
|
|
1023
|
+
que_type: timeColumn.que_type
|
|
1024
|
+
}
|
|
1025
|
+
]
|
|
1026
|
+
: [])
|
|
1027
|
+
];
|
|
1028
|
+
if (!summaryMeta) {
|
|
1029
|
+
throw new Error("Failed to build summary metadata");
|
|
1030
|
+
}
|
|
1031
|
+
return {
|
|
1032
|
+
data: {
|
|
1033
|
+
summary: {
|
|
1034
|
+
total_count: scannedRecords,
|
|
1035
|
+
total_amount: amountColumn ? totalAmount : null,
|
|
1036
|
+
by_day: byDayStats,
|
|
1037
|
+
missing_count: missingCount
|
|
1038
|
+
},
|
|
1039
|
+
rows,
|
|
1040
|
+
meta: {
|
|
1041
|
+
field_mapping: fieldMapping,
|
|
1042
|
+
filters: {
|
|
1043
|
+
app_key: args.app_key,
|
|
1044
|
+
time_range: timeColumn
|
|
1045
|
+
? {
|
|
1046
|
+
column: timeColumn.requested,
|
|
1047
|
+
from: args.time_range?.from ?? null,
|
|
1048
|
+
to: args.time_range?.to ?? null,
|
|
1049
|
+
timezone
|
|
1050
|
+
}
|
|
1051
|
+
: null
|
|
1052
|
+
},
|
|
1053
|
+
stat_policy: {
|
|
1054
|
+
include_negative: includeNegative,
|
|
1055
|
+
include_null: includeNull
|
|
1056
|
+
},
|
|
1057
|
+
execution: {
|
|
1058
|
+
scanned_records: scannedRecords,
|
|
1059
|
+
scanned_pages: scannedPages,
|
|
1060
|
+
truncated,
|
|
1061
|
+
row_cap: rowCap,
|
|
1062
|
+
column_cap: args.max_columns ?? null,
|
|
1063
|
+
scan_max_pages: scanMaxPages
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
meta: summaryMeta,
|
|
1068
|
+
message: `Summarized ${scannedRecords} records`
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
function resolveSummaryColumns(columns, index, label) {
|
|
1072
|
+
return normalizeColumnSelectors(columns).map((requested) => resolveSummaryColumn(requested, index, label));
|
|
1073
|
+
}
|
|
1074
|
+
function resolveSummaryColumn(column, index, label) {
|
|
1075
|
+
const requested = String(column).trim();
|
|
1076
|
+
if (!requested) {
|
|
1077
|
+
throw new Error(`${label} contains an empty column selector`);
|
|
1078
|
+
}
|
|
1079
|
+
if (isNumericKey(requested)) {
|
|
1080
|
+
const hit = index.byId.get(String(Number(requested)));
|
|
1081
|
+
if (!hit) {
|
|
1082
|
+
throw new Error(`${label} references unknown que_id "${requested}"`);
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
requested,
|
|
1086
|
+
que_id: normalizeQueId(hit.queId),
|
|
1087
|
+
que_title: asNullableString(hit.queTitle),
|
|
1088
|
+
que_type: hit.queType
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
const hit = resolveFieldByKey(requested, index);
|
|
1092
|
+
if (!hit || hit.queId === undefined || hit.queId === null) {
|
|
1093
|
+
throw new Error(`${label} cannot resolve field "${requested}"`);
|
|
1094
|
+
}
|
|
1095
|
+
return {
|
|
1096
|
+
requested,
|
|
1097
|
+
que_id: normalizeQueId(hit.queId),
|
|
1098
|
+
que_title: asNullableString(hit.queTitle),
|
|
1099
|
+
que_type: hit.queType
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function buildSummaryRow(answers, columns) {
|
|
1103
|
+
const row = {};
|
|
1104
|
+
for (const column of columns) {
|
|
1105
|
+
row[column.requested] = extractSummaryColumnValue(answers, column);
|
|
1106
|
+
}
|
|
1107
|
+
return row;
|
|
1108
|
+
}
|
|
1109
|
+
function extractSummaryColumnValue(answers, column) {
|
|
1110
|
+
const targetId = normalizeColumnSelector(String(column.que_id));
|
|
1111
|
+
const targetTitle = column.que_title ? normalizeColumnSelector(column.que_title) : null;
|
|
1112
|
+
for (const answerRaw of answers) {
|
|
1113
|
+
const answer = asObject(answerRaw);
|
|
1114
|
+
if (!answer) {
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
const answerQueId = asNullableString(answer.queId);
|
|
1118
|
+
if (answerQueId && normalizeColumnSelector(answerQueId) === targetId) {
|
|
1119
|
+
return extractAnswerDisplayValue(answer);
|
|
1120
|
+
}
|
|
1121
|
+
if (targetTitle) {
|
|
1122
|
+
const answerQueTitle = asNullableString(answer.queTitle);
|
|
1123
|
+
if (answerQueTitle && normalizeColumnSelector(answerQueTitle) === targetTitle) {
|
|
1124
|
+
return extractAnswerDisplayValue(answer);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
function extractAnswerDisplayValue(answer) {
|
|
1131
|
+
const tableValues = answer.tableValues ?? answer.table_values;
|
|
1132
|
+
if (tableValues !== undefined) {
|
|
1133
|
+
return tableValues;
|
|
1134
|
+
}
|
|
1135
|
+
const values = asArray(answer.values);
|
|
1136
|
+
if (values.length === 0) {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
const normalized = values.map((item) => extractAnswerValueCell(item));
|
|
1140
|
+
return normalized.length === 1 ? normalized[0] : normalized;
|
|
1141
|
+
}
|
|
1142
|
+
function extractAnswerValueCell(value) {
|
|
1143
|
+
const obj = asObject(value);
|
|
1144
|
+
if (!obj) {
|
|
1145
|
+
return value;
|
|
1146
|
+
}
|
|
1147
|
+
if (obj.dataValue !== undefined) {
|
|
1148
|
+
return obj.dataValue;
|
|
1149
|
+
}
|
|
1150
|
+
if (obj.value !== undefined) {
|
|
1151
|
+
return obj.value;
|
|
1152
|
+
}
|
|
1153
|
+
if (obj.valueStr !== undefined) {
|
|
1154
|
+
return obj.valueStr;
|
|
1155
|
+
}
|
|
1156
|
+
return obj;
|
|
1157
|
+
}
|
|
1158
|
+
function toFiniteAmount(value) {
|
|
1159
|
+
if (value === null || value === undefined) {
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
if (Array.isArray(value)) {
|
|
1163
|
+
if (value.length !== 1) {
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
return toFiniteAmount(value[0]);
|
|
1167
|
+
}
|
|
1168
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1169
|
+
return value;
|
|
1170
|
+
}
|
|
1171
|
+
if (typeof value === "string") {
|
|
1172
|
+
const normalized = value.replace(/,/g, "").trim();
|
|
1173
|
+
if (!normalized) {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
const parsed = Number(normalized);
|
|
1177
|
+
if (Number.isFinite(parsed)) {
|
|
1178
|
+
return parsed;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
function toDayBucket(value, timezone) {
|
|
1184
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
1185
|
+
if (first === null || first === undefined) {
|
|
1186
|
+
return "unknown";
|
|
1187
|
+
}
|
|
1188
|
+
if (typeof first === "string") {
|
|
1189
|
+
const trimmed = first.trim();
|
|
1190
|
+
const direct = trimmed.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
1191
|
+
if (direct) {
|
|
1192
|
+
return direct[1];
|
|
1193
|
+
}
|
|
1194
|
+
const parsed = new Date(trimmed);
|
|
1195
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
1196
|
+
return formatDateBucket(parsed, timezone);
|
|
1197
|
+
}
|
|
1198
|
+
return "unknown";
|
|
1199
|
+
}
|
|
1200
|
+
if (typeof first === "number") {
|
|
1201
|
+
const parsed = new Date(first);
|
|
1202
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
1203
|
+
return formatDateBucket(parsed, timezone);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return "unknown";
|
|
1207
|
+
}
|
|
1208
|
+
function formatDateBucket(value, timezone) {
|
|
1209
|
+
try {
|
|
1210
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
1211
|
+
timeZone: timezone,
|
|
1212
|
+
year: "numeric",
|
|
1213
|
+
month: "2-digit",
|
|
1214
|
+
day: "2-digit"
|
|
1215
|
+
}).format(value);
|
|
1216
|
+
}
|
|
1217
|
+
catch {
|
|
1218
|
+
return value.toISOString().slice(0, 10);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
621
1221
|
function buildListPayload(params) {
|
|
622
1222
|
const payload = {
|
|
623
1223
|
pageNum: params.pageNum,
|
|
@@ -926,12 +1526,17 @@ function projectRecordItemsColumns(params) {
|
|
|
926
1526
|
return {
|
|
927
1527
|
items: params.items,
|
|
928
1528
|
reason: null,
|
|
929
|
-
selectedColumns:
|
|
1529
|
+
selectedColumns: [],
|
|
1530
|
+
matchedAnswersCount: 0
|
|
930
1531
|
};
|
|
931
1532
|
}
|
|
932
1533
|
const normalizedSelectors = normalizeColumnSelectors(params.selectColumns);
|
|
1534
|
+
if (normalizedSelectors.length === 0) {
|
|
1535
|
+
throw new Error("select_columns must contain at least one non-empty column identifier");
|
|
1536
|
+
}
|
|
933
1537
|
const selectorSet = new Set(normalizedSelectors.map((item) => normalizeColumnSelector(item)));
|
|
934
1538
|
let columnCapped = false;
|
|
1539
|
+
let matchedAnswersCount = 0;
|
|
935
1540
|
const projectedItems = params.items.map((item) => {
|
|
936
1541
|
const answers = asArray(item.answers);
|
|
937
1542
|
let projected = answers;
|
|
@@ -942,6 +1547,7 @@ function projectRecordItemsColumns(params) {
|
|
|
942
1547
|
projected = projected.slice(0, params.maxColumns);
|
|
943
1548
|
columnCapped = true;
|
|
944
1549
|
}
|
|
1550
|
+
matchedAnswersCount += projected.length;
|
|
945
1551
|
return {
|
|
946
1552
|
...item,
|
|
947
1553
|
answers: projected
|
|
@@ -953,7 +1559,8 @@ function projectRecordItemsColumns(params) {
|
|
|
953
1559
|
return {
|
|
954
1560
|
items: projectedItems,
|
|
955
1561
|
reason,
|
|
956
|
-
selectedColumns: normalizedSelectors
|
|
1562
|
+
selectedColumns: normalizedSelectors,
|
|
1563
|
+
matchedAnswersCount
|
|
957
1564
|
};
|
|
958
1565
|
}
|
|
959
1566
|
function projectAnswersForOutput(params) {
|
|
@@ -1071,7 +1678,7 @@ function errorResult(error) {
|
|
|
1071
1678
|
const payload = toErrorPayload(error);
|
|
1072
1679
|
return {
|
|
1073
1680
|
isError: true,
|
|
1074
|
-
|
|
1681
|
+
// Keep error payload in text to avoid outputSchema(success) validation conflicts across MCP clients.
|
|
1075
1682
|
content: [
|
|
1076
1683
|
{
|
|
1077
1684
|
type: "text",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qingflow-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsc -p tsconfig.json",
|
|
34
|
+
"test": "npm run build && node --test \"test/*.test.js\"",
|
|
34
35
|
"dev": "tsx src/server.ts",
|
|
35
36
|
"start": "node dist/server.js",
|
|
36
37
|
"prepublishOnly": "npm run build"
|