qingflow-mcp 0.2.5 → 0.2.7
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 +29 -0
- package/dist/server.js +717 -97
- 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,6 +110,28 @@ 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
|
+
3. In `list` mode, `time_range` is translated to list filters when `from` or `to` is provided.
|
|
123
|
+
|
|
124
|
+
Summary mode output:
|
|
125
|
+
|
|
126
|
+
1. `summary`: aggregated stats (`total_count`, `total_amount`, `by_day`, `missing_count`).
|
|
127
|
+
2. `rows`: strict column rows (only requested `select_columns`).
|
|
128
|
+
3. `meta`: field mapping, filter scope, stat policy, execution limits.
|
|
129
|
+
|
|
130
|
+
Return shape:
|
|
131
|
+
|
|
132
|
+
1. success: structured payload `{ "ok": true, "data": ..., "meta": ... }`
|
|
133
|
+
2. failure: MCP `isError=true`, and text content is JSON payload like `{ "ok": false, "message": ..., ... }`
|
|
134
|
+
|
|
106
135
|
## List Query Tips
|
|
107
136
|
|
|
108
137
|
Strict mode (`qf_records_list`):
|
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.7"
|
|
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,6 +148,7 @@ const formOutputSchema = z.object({
|
|
|
147
148
|
}),
|
|
148
149
|
meta: apiMetaSchema
|
|
149
150
|
});
|
|
151
|
+
const formOutputSchema = formSuccessOutputSchema;
|
|
150
152
|
const listInputSchema = z
|
|
151
153
|
.object({
|
|
152
154
|
app_key: z.string().min(1),
|
|
@@ -201,7 +203,7 @@ const listInputSchema = z
|
|
|
201
203
|
.refine((value) => value.include_answers !== false, {
|
|
202
204
|
message: "include_answers=false is not allowed in strict column mode"
|
|
203
205
|
});
|
|
204
|
-
const
|
|
206
|
+
const listSuccessOutputSchema = z.object({
|
|
205
207
|
ok: z.literal(true),
|
|
206
208
|
data: z.object({
|
|
207
209
|
app_key: z.string(),
|
|
@@ -223,6 +225,7 @@ const listOutputSchema = z.object({
|
|
|
223
225
|
}),
|
|
224
226
|
meta: apiMetaSchema
|
|
225
227
|
});
|
|
228
|
+
const listOutputSchema = listSuccessOutputSchema;
|
|
226
229
|
const recordGetInputSchema = z.object({
|
|
227
230
|
apply_id: z.union([z.string().min(1), z.number().int()]),
|
|
228
231
|
max_columns: z.number().int().positive().max(200).optional(),
|
|
@@ -232,7 +235,7 @@ const recordGetInputSchema = z.object({
|
|
|
232
235
|
.max(200)
|
|
233
236
|
.optional()
|
|
234
237
|
});
|
|
235
|
-
const
|
|
238
|
+
const recordGetSuccessOutputSchema = z.object({
|
|
236
239
|
ok: z.literal(true),
|
|
237
240
|
data: z.object({
|
|
238
241
|
apply_id: z.union([z.string(), z.number(), z.null()]),
|
|
@@ -247,6 +250,7 @@ const recordGetOutputSchema = z.object({
|
|
|
247
250
|
}),
|
|
248
251
|
meta: apiMetaSchema
|
|
249
252
|
});
|
|
253
|
+
const recordGetOutputSchema = recordGetSuccessOutputSchema;
|
|
250
254
|
const createInputSchema = z
|
|
251
255
|
.object({
|
|
252
256
|
app_key: z.string().min(1),
|
|
@@ -266,7 +270,7 @@ const createInputSchema = z
|
|
|
266
270
|
.refine((value) => hasWritePayload(value.answers, value.fields), {
|
|
267
271
|
message: "Either answers or fields is required"
|
|
268
272
|
});
|
|
269
|
-
const
|
|
273
|
+
const createSuccessOutputSchema = z.object({
|
|
270
274
|
ok: z.literal(true),
|
|
271
275
|
data: z.object({
|
|
272
276
|
request_id: z.string().nullable(),
|
|
@@ -275,6 +279,7 @@ const createOutputSchema = z.object({
|
|
|
275
279
|
}),
|
|
276
280
|
meta: apiMetaSchema
|
|
277
281
|
});
|
|
282
|
+
const createOutputSchema = createSuccessOutputSchema;
|
|
278
283
|
const updateInputSchema = z
|
|
279
284
|
.object({
|
|
280
285
|
apply_id: z.union([z.string().min(1), z.number().int()]),
|
|
@@ -287,7 +292,7 @@ const updateInputSchema = z
|
|
|
287
292
|
.refine((value) => hasWritePayload(value.answers, value.fields), {
|
|
288
293
|
message: "Either answers or fields is required"
|
|
289
294
|
});
|
|
290
|
-
const
|
|
295
|
+
const updateSuccessOutputSchema = z.object({
|
|
291
296
|
ok: z.literal(true),
|
|
292
297
|
data: z.object({
|
|
293
298
|
request_id: z.string().nullable(),
|
|
@@ -295,14 +300,144 @@ const updateOutputSchema = z.object({
|
|
|
295
300
|
}),
|
|
296
301
|
meta: apiMetaSchema
|
|
297
302
|
});
|
|
303
|
+
const updateOutputSchema = updateSuccessOutputSchema;
|
|
298
304
|
const operationInputSchema = z.object({
|
|
299
305
|
request_id: z.string().min(1)
|
|
300
306
|
});
|
|
301
|
-
const
|
|
307
|
+
const operationSuccessOutputSchema = z.object({
|
|
302
308
|
ok: z.literal(true),
|
|
303
309
|
data: operationResultSchema,
|
|
304
310
|
meta: apiMetaSchema
|
|
305
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;
|
|
306
441
|
server.registerTool("qf_apps_list", {
|
|
307
442
|
title: "Qingflow Apps List",
|
|
308
443
|
description: "List Qingflow apps with optional filtering and client-side slicing.",
|
|
@@ -390,73 +525,8 @@ server.registerTool("qf_records_list", {
|
|
|
390
525
|
}
|
|
391
526
|
}, async (args) => {
|
|
392
527
|
try {
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
|
|
396
|
-
const includeAnswers = true;
|
|
397
|
-
const payload = buildListPayload({
|
|
398
|
-
pageNum,
|
|
399
|
-
pageSize,
|
|
400
|
-
mode: args.mode,
|
|
401
|
-
type: args.type,
|
|
402
|
-
keyword: args.keyword,
|
|
403
|
-
queryLogic: args.query_logic,
|
|
404
|
-
applyIds: args.apply_ids,
|
|
405
|
-
sort: normalizedSort,
|
|
406
|
-
filters: args.filters
|
|
407
|
-
});
|
|
408
|
-
const response = await client.listRecords(args.app_key, payload, { userId: args.user_id });
|
|
409
|
-
const result = asObject(response.result);
|
|
410
|
-
const rawItems = asArray(result?.result);
|
|
411
|
-
const listLimit = resolveListItemLimit({
|
|
412
|
-
total: rawItems.length,
|
|
413
|
-
requestedMaxRows: args.max_rows,
|
|
414
|
-
requestedMaxItems: args.max_items,
|
|
415
|
-
includeAnswers
|
|
416
|
-
});
|
|
417
|
-
const items = rawItems
|
|
418
|
-
.slice(0, listLimit.limit)
|
|
419
|
-
.map((raw) => normalizeRecordItem(raw, includeAnswers));
|
|
420
|
-
const columnProjection = projectRecordItemsColumns({
|
|
421
|
-
items,
|
|
422
|
-
includeAnswers,
|
|
423
|
-
maxColumns: args.max_columns,
|
|
424
|
-
selectColumns: args.select_columns
|
|
425
|
-
});
|
|
426
|
-
if (items.length > 0 && columnProjection.matchedAnswersCount === 0) {
|
|
427
|
-
throw new Error(`No answers matched select_columns (${args.select_columns
|
|
428
|
-
.map((item) => String(item))
|
|
429
|
-
.join(", ")}). Check que_id/title from qf_form_get.`);
|
|
430
|
-
}
|
|
431
|
-
const fitted = fitListItemsWithinSize({
|
|
432
|
-
items: columnProjection.items,
|
|
433
|
-
limitBytes: MAX_LIST_ITEMS_BYTES
|
|
434
|
-
});
|
|
435
|
-
const truncationReason = mergeTruncationReasons(listLimit.reason, columnProjection.reason, fitted.reason);
|
|
436
|
-
return okResult({
|
|
437
|
-
ok: true,
|
|
438
|
-
data: {
|
|
439
|
-
app_key: args.app_key,
|
|
440
|
-
pagination: {
|
|
441
|
-
page_num: toPositiveInt(result?.pageNum) ?? pageNum,
|
|
442
|
-
page_size: toPositiveInt(result?.pageSize) ?? pageSize,
|
|
443
|
-
page_amount: toNonNegativeInt(result?.pageAmount),
|
|
444
|
-
result_amount: toNonNegativeInt(result?.resultAmount) ?? fitted.items.length
|
|
445
|
-
},
|
|
446
|
-
items: fitted.items,
|
|
447
|
-
applied_limits: {
|
|
448
|
-
include_answers: includeAnswers,
|
|
449
|
-
row_cap: listLimit.limit,
|
|
450
|
-
column_cap: args.max_columns ?? null,
|
|
451
|
-
selected_columns: columnProjection.selectedColumns
|
|
452
|
-
}
|
|
453
|
-
},
|
|
454
|
-
meta: buildMeta(response)
|
|
455
|
-
}, buildRecordsListMessage({
|
|
456
|
-
returned: fitted.items.length,
|
|
457
|
-
total: rawItems.length,
|
|
458
|
-
truncationReason
|
|
459
|
-
}));
|
|
528
|
+
const executed = await executeRecordsList(args);
|
|
529
|
+
return okResult(executed.payload, executed.message);
|
|
460
530
|
}
|
|
461
531
|
catch (error) {
|
|
462
532
|
return errorResult(error);
|
|
@@ -473,31 +543,61 @@ server.registerTool("qf_record_get", {
|
|
|
473
543
|
}
|
|
474
544
|
}, async (args) => {
|
|
475
545
|
try {
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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);
|
|
488
592
|
return okResult({
|
|
489
593
|
ok: true,
|
|
490
594
|
data: {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
applied_limits: {
|
|
495
|
-
column_cap: args.max_columns ?? null,
|
|
496
|
-
selected_columns: projection.selectedColumns
|
|
497
|
-
}
|
|
595
|
+
mode: "list",
|
|
596
|
+
source_tool: "qf_records_list",
|
|
597
|
+
list: executed.payload.data
|
|
498
598
|
},
|
|
499
|
-
meta:
|
|
500
|
-
},
|
|
599
|
+
meta: executed.payload.meta
|
|
600
|
+
}, executed.message);
|
|
501
601
|
}
|
|
502
602
|
catch (error) {
|
|
503
603
|
return errorResult(error);
|
|
@@ -624,6 +724,526 @@ function buildMeta(response) {
|
|
|
624
724
|
base_url: baseUrl
|
|
625
725
|
};
|
|
626
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
|
+
const filters = buildListFiltersFromQuery(args);
|
|
751
|
+
return listInputSchema.parse({
|
|
752
|
+
app_key: args.app_key,
|
|
753
|
+
user_id: args.user_id,
|
|
754
|
+
page_num: args.page_num,
|
|
755
|
+
page_size: args.page_size,
|
|
756
|
+
mode: args.mode,
|
|
757
|
+
type: args.type,
|
|
758
|
+
keyword: args.keyword,
|
|
759
|
+
query_logic: args.query_logic,
|
|
760
|
+
apply_ids: args.apply_ids,
|
|
761
|
+
sort: args.sort,
|
|
762
|
+
filters,
|
|
763
|
+
max_rows: args.max_rows,
|
|
764
|
+
max_items: args.max_items,
|
|
765
|
+
max_columns: args.max_columns,
|
|
766
|
+
select_columns: args.select_columns,
|
|
767
|
+
include_answers: args.include_answers
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
function buildListFiltersFromQuery(args) {
|
|
771
|
+
const filters = [...(args.filters ?? [])];
|
|
772
|
+
const timeRange = args.time_range;
|
|
773
|
+
if (!timeRange) {
|
|
774
|
+
return filters.length > 0 ? filters : undefined;
|
|
775
|
+
}
|
|
776
|
+
if (timeRange.from === undefined && timeRange.to === undefined) {
|
|
777
|
+
return filters.length > 0 ? filters : undefined;
|
|
778
|
+
}
|
|
779
|
+
const timeSelector = normalizeColumnSelector(timeRange.column);
|
|
780
|
+
const alreadyHasTimeFilter = filters.some((item) => {
|
|
781
|
+
if (item.que_id === undefined) {
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
return normalizeColumnSelector(item.que_id) === timeSelector;
|
|
785
|
+
});
|
|
786
|
+
if (!alreadyHasTimeFilter) {
|
|
787
|
+
filters.push({
|
|
788
|
+
que_id: timeRange.column,
|
|
789
|
+
...(timeRange.from !== undefined ? { min_value: timeRange.from } : {}),
|
|
790
|
+
...(timeRange.to !== undefined ? { max_value: timeRange.to } : {})
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
return filters.length > 0 ? filters : undefined;
|
|
794
|
+
}
|
|
795
|
+
function buildRecordGetArgsFromQuery(args) {
|
|
796
|
+
if (args.apply_id === undefined) {
|
|
797
|
+
throw new Error("apply_id is required for record query");
|
|
798
|
+
}
|
|
799
|
+
return recordGetInputSchema.parse({
|
|
800
|
+
apply_id: args.apply_id,
|
|
801
|
+
max_columns: args.max_columns,
|
|
802
|
+
select_columns: args.select_columns
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
async function executeRecordsList(args) {
|
|
806
|
+
const pageNum = args.page_num ?? 1;
|
|
807
|
+
const pageSize = args.page_size ?? DEFAULT_PAGE_SIZE;
|
|
808
|
+
const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
|
|
809
|
+
const includeAnswers = true;
|
|
810
|
+
const payload = buildListPayload({
|
|
811
|
+
pageNum,
|
|
812
|
+
pageSize,
|
|
813
|
+
mode: args.mode,
|
|
814
|
+
type: args.type,
|
|
815
|
+
keyword: args.keyword,
|
|
816
|
+
queryLogic: args.query_logic,
|
|
817
|
+
applyIds: args.apply_ids,
|
|
818
|
+
sort: normalizedSort,
|
|
819
|
+
filters: args.filters
|
|
820
|
+
});
|
|
821
|
+
const response = await client.listRecords(args.app_key, payload, { userId: args.user_id });
|
|
822
|
+
const result = asObject(response.result);
|
|
823
|
+
const rawItems = asArray(result?.result);
|
|
824
|
+
const listLimit = resolveListItemLimit({
|
|
825
|
+
total: rawItems.length,
|
|
826
|
+
requestedMaxRows: args.max_rows,
|
|
827
|
+
requestedMaxItems: args.max_items,
|
|
828
|
+
includeAnswers
|
|
829
|
+
});
|
|
830
|
+
const items = rawItems
|
|
831
|
+
.slice(0, listLimit.limit)
|
|
832
|
+
.map((raw) => normalizeRecordItem(raw, includeAnswers));
|
|
833
|
+
const columnProjection = projectRecordItemsColumns({
|
|
834
|
+
items,
|
|
835
|
+
includeAnswers,
|
|
836
|
+
maxColumns: args.max_columns,
|
|
837
|
+
selectColumns: args.select_columns
|
|
838
|
+
});
|
|
839
|
+
if (items.length > 0 && columnProjection.matchedAnswersCount === 0) {
|
|
840
|
+
throw new Error(`No answers matched select_columns (${args.select_columns
|
|
841
|
+
.map((item) => String(item))
|
|
842
|
+
.join(", ")}). Check que_id/title from qf_form_get.`);
|
|
843
|
+
}
|
|
844
|
+
const fitted = fitListItemsWithinSize({
|
|
845
|
+
items: columnProjection.items,
|
|
846
|
+
limitBytes: MAX_LIST_ITEMS_BYTES
|
|
847
|
+
});
|
|
848
|
+
const truncationReason = mergeTruncationReasons(listLimit.reason, columnProjection.reason, fitted.reason);
|
|
849
|
+
const responsePayload = {
|
|
850
|
+
ok: true,
|
|
851
|
+
data: {
|
|
852
|
+
app_key: args.app_key,
|
|
853
|
+
pagination: {
|
|
854
|
+
page_num: toPositiveInt(result?.pageNum) ?? pageNum,
|
|
855
|
+
page_size: toPositiveInt(result?.pageSize) ?? pageSize,
|
|
856
|
+
page_amount: toNonNegativeInt(result?.pageAmount),
|
|
857
|
+
result_amount: toNonNegativeInt(result?.resultAmount) ?? fitted.items.length
|
|
858
|
+
},
|
|
859
|
+
items: fitted.items,
|
|
860
|
+
applied_limits: {
|
|
861
|
+
include_answers: includeAnswers,
|
|
862
|
+
row_cap: listLimit.limit,
|
|
863
|
+
column_cap: args.max_columns ?? null,
|
|
864
|
+
selected_columns: columnProjection.selectedColumns
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
meta: buildMeta(response)
|
|
868
|
+
};
|
|
869
|
+
return {
|
|
870
|
+
payload: responsePayload,
|
|
871
|
+
message: buildRecordsListMessage({
|
|
872
|
+
returned: fitted.items.length,
|
|
873
|
+
total: rawItems.length,
|
|
874
|
+
truncationReason
|
|
875
|
+
})
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
async function executeRecordGet(args) {
|
|
879
|
+
const response = await client.getRecord(String(args.apply_id));
|
|
880
|
+
const record = asObject(response.result) ?? {};
|
|
881
|
+
const projection = projectAnswersForOutput({
|
|
882
|
+
answers: asArray(record.answers),
|
|
883
|
+
maxColumns: args.max_columns,
|
|
884
|
+
selectColumns: args.select_columns
|
|
885
|
+
});
|
|
886
|
+
const projectedRecord = {
|
|
887
|
+
...record,
|
|
888
|
+
answers: projection.answers
|
|
889
|
+
};
|
|
890
|
+
const answerCount = projection.answers.length;
|
|
891
|
+
return {
|
|
892
|
+
payload: {
|
|
893
|
+
ok: true,
|
|
894
|
+
data: {
|
|
895
|
+
apply_id: record.applyId ?? null,
|
|
896
|
+
answer_count: answerCount,
|
|
897
|
+
record: projectedRecord,
|
|
898
|
+
applied_limits: {
|
|
899
|
+
column_cap: args.max_columns ?? null,
|
|
900
|
+
selected_columns: projection.selectedColumns
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
meta: buildMeta(response)
|
|
904
|
+
},
|
|
905
|
+
message: `Fetched record ${String(args.apply_id)}`
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
async function executeRecordsSummary(args) {
|
|
909
|
+
if (!args.app_key) {
|
|
910
|
+
throw new Error("app_key is required for summary query");
|
|
911
|
+
}
|
|
912
|
+
if (!args.select_columns?.length) {
|
|
913
|
+
throw new Error("select_columns is required for summary query");
|
|
914
|
+
}
|
|
915
|
+
const includeNegative = args.stat_policy?.include_negative ?? true;
|
|
916
|
+
const includeNull = args.stat_policy?.include_null ?? false;
|
|
917
|
+
const scanMaxPages = args.scan_max_pages ?? 50;
|
|
918
|
+
const pageSize = args.page_size ?? DEFAULT_PAGE_SIZE;
|
|
919
|
+
const rowCap = Math.min(args.max_rows ?? DEFAULT_PAGE_SIZE, 200);
|
|
920
|
+
const timezone = args.time_range?.timezone ?? "Asia/Shanghai";
|
|
921
|
+
const form = await getFormCached(args.app_key, args.user_id, false);
|
|
922
|
+
const index = buildFieldIndex(form.result);
|
|
923
|
+
const selectedColumns = resolveSummaryColumns(args.select_columns, index, "select_columns");
|
|
924
|
+
const effectiveColumns = args.max_columns !== undefined ? selectedColumns.slice(0, args.max_columns) : selectedColumns;
|
|
925
|
+
if (effectiveColumns.length === 0) {
|
|
926
|
+
throw new Error("No output columns remain after max_columns cap");
|
|
927
|
+
}
|
|
928
|
+
const amountColumn = args.amount_column !== undefined
|
|
929
|
+
? resolveSummaryColumn(args.amount_column, index, "amount_column")
|
|
930
|
+
: null;
|
|
931
|
+
const timeColumn = args.time_range ? resolveSummaryColumn(args.time_range.column, index, "time_range.column") : null;
|
|
932
|
+
const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
|
|
933
|
+
const summaryFilters = [...(args.filters ?? [])];
|
|
934
|
+
if (timeColumn && (args.time_range?.from || args.time_range?.to)) {
|
|
935
|
+
summaryFilters.push({
|
|
936
|
+
que_id: timeColumn.que_id,
|
|
937
|
+
...(args.time_range.from ? { min_value: args.time_range.from } : {}),
|
|
938
|
+
...(args.time_range.to ? { max_value: args.time_range.to } : {})
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
let currentPage = args.page_num ?? 1;
|
|
942
|
+
let scannedPages = 0;
|
|
943
|
+
let scannedRecords = 0;
|
|
944
|
+
let truncated = false;
|
|
945
|
+
let summaryMeta = null;
|
|
946
|
+
let totalAmount = 0;
|
|
947
|
+
let missingCount = 0;
|
|
948
|
+
const rows = [];
|
|
949
|
+
const byDay = new Map();
|
|
950
|
+
while (true) {
|
|
951
|
+
const payload = buildListPayload({
|
|
952
|
+
pageNum: currentPage,
|
|
953
|
+
pageSize,
|
|
954
|
+
mode: args.mode,
|
|
955
|
+
type: args.type,
|
|
956
|
+
keyword: args.keyword,
|
|
957
|
+
queryLogic: args.query_logic,
|
|
958
|
+
applyIds: args.apply_ids,
|
|
959
|
+
sort: normalizedSort,
|
|
960
|
+
filters: summaryFilters
|
|
961
|
+
});
|
|
962
|
+
const response = await client.listRecords(args.app_key, payload, { userId: args.user_id });
|
|
963
|
+
summaryMeta = summaryMeta ?? buildMeta(response);
|
|
964
|
+
scannedPages += 1;
|
|
965
|
+
const result = asObject(response.result);
|
|
966
|
+
const rawItems = asArray(result?.result);
|
|
967
|
+
const pageAmount = toPositiveInt(result?.pageAmount);
|
|
968
|
+
const hasMoreByAmount = pageAmount !== null ? currentPage < pageAmount : rawItems.length === pageSize;
|
|
969
|
+
for (const rawItem of rawItems) {
|
|
970
|
+
const record = asObject(rawItem) ?? {};
|
|
971
|
+
const answers = asArray(record.answers);
|
|
972
|
+
scannedRecords += 1;
|
|
973
|
+
if (rows.length < rowCap) {
|
|
974
|
+
rows.push(buildSummaryRow(answers, effectiveColumns));
|
|
975
|
+
}
|
|
976
|
+
let amountContribution = 0;
|
|
977
|
+
let hasAmountContribution = false;
|
|
978
|
+
if (amountColumn) {
|
|
979
|
+
const amountValue = extractSummaryColumnValue(answers, amountColumn);
|
|
980
|
+
const numericAmount = toFiniteAmount(amountValue);
|
|
981
|
+
if (numericAmount === null) {
|
|
982
|
+
if (!includeNull) {
|
|
983
|
+
missingCount += 1;
|
|
984
|
+
}
|
|
985
|
+
else {
|
|
986
|
+
hasAmountContribution = true;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
else if (includeNegative || numericAmount >= 0) {
|
|
990
|
+
amountContribution = numericAmount;
|
|
991
|
+
hasAmountContribution = true;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (hasAmountContribution) {
|
|
995
|
+
totalAmount += amountContribution;
|
|
996
|
+
}
|
|
997
|
+
const dayKey = timeColumn
|
|
998
|
+
? toDayBucket(extractSummaryColumnValue(answers, timeColumn), timezone)
|
|
999
|
+
: "all";
|
|
1000
|
+
const bucket = byDay.get(dayKey) ?? { count: 0, amount: 0 };
|
|
1001
|
+
bucket.count += 1;
|
|
1002
|
+
if (amountColumn && hasAmountContribution) {
|
|
1003
|
+
bucket.amount += amountContribution;
|
|
1004
|
+
}
|
|
1005
|
+
byDay.set(dayKey, bucket);
|
|
1006
|
+
}
|
|
1007
|
+
if (!hasMoreByAmount) {
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
1010
|
+
if (scannedPages >= scanMaxPages) {
|
|
1011
|
+
truncated = true;
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
currentPage += 1;
|
|
1015
|
+
}
|
|
1016
|
+
const byDayStats = Array.from(byDay.entries())
|
|
1017
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
1018
|
+
.map(([day, bucket]) => ({
|
|
1019
|
+
day,
|
|
1020
|
+
count: bucket.count,
|
|
1021
|
+
amount_total: amountColumn ? bucket.amount : null
|
|
1022
|
+
}));
|
|
1023
|
+
const fieldMapping = [
|
|
1024
|
+
...effectiveColumns.map((item) => ({
|
|
1025
|
+
role: "row",
|
|
1026
|
+
requested: item.requested,
|
|
1027
|
+
que_id: item.que_id,
|
|
1028
|
+
que_title: item.que_title,
|
|
1029
|
+
que_type: item.que_type
|
|
1030
|
+
})),
|
|
1031
|
+
...(amountColumn
|
|
1032
|
+
? [
|
|
1033
|
+
{
|
|
1034
|
+
role: "amount",
|
|
1035
|
+
requested: amountColumn.requested,
|
|
1036
|
+
que_id: amountColumn.que_id,
|
|
1037
|
+
que_title: amountColumn.que_title,
|
|
1038
|
+
que_type: amountColumn.que_type
|
|
1039
|
+
}
|
|
1040
|
+
]
|
|
1041
|
+
: []),
|
|
1042
|
+
...(timeColumn
|
|
1043
|
+
? [
|
|
1044
|
+
{
|
|
1045
|
+
role: "time",
|
|
1046
|
+
requested: timeColumn.requested,
|
|
1047
|
+
que_id: timeColumn.que_id,
|
|
1048
|
+
que_title: timeColumn.que_title,
|
|
1049
|
+
que_type: timeColumn.que_type
|
|
1050
|
+
}
|
|
1051
|
+
]
|
|
1052
|
+
: [])
|
|
1053
|
+
];
|
|
1054
|
+
if (!summaryMeta) {
|
|
1055
|
+
throw new Error("Failed to build summary metadata");
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
data: {
|
|
1059
|
+
summary: {
|
|
1060
|
+
total_count: scannedRecords,
|
|
1061
|
+
total_amount: amountColumn ? totalAmount : null,
|
|
1062
|
+
by_day: byDayStats,
|
|
1063
|
+
missing_count: missingCount
|
|
1064
|
+
},
|
|
1065
|
+
rows,
|
|
1066
|
+
meta: {
|
|
1067
|
+
field_mapping: fieldMapping,
|
|
1068
|
+
filters: {
|
|
1069
|
+
app_key: args.app_key,
|
|
1070
|
+
time_range: timeColumn
|
|
1071
|
+
? {
|
|
1072
|
+
column: timeColumn.requested,
|
|
1073
|
+
from: args.time_range?.from ?? null,
|
|
1074
|
+
to: args.time_range?.to ?? null,
|
|
1075
|
+
timezone
|
|
1076
|
+
}
|
|
1077
|
+
: null
|
|
1078
|
+
},
|
|
1079
|
+
stat_policy: {
|
|
1080
|
+
include_negative: includeNegative,
|
|
1081
|
+
include_null: includeNull
|
|
1082
|
+
},
|
|
1083
|
+
execution: {
|
|
1084
|
+
scanned_records: scannedRecords,
|
|
1085
|
+
scanned_pages: scannedPages,
|
|
1086
|
+
truncated,
|
|
1087
|
+
row_cap: rowCap,
|
|
1088
|
+
column_cap: args.max_columns ?? null,
|
|
1089
|
+
scan_max_pages: scanMaxPages
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
},
|
|
1093
|
+
meta: summaryMeta,
|
|
1094
|
+
message: `Summarized ${scannedRecords} records`
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
function resolveSummaryColumns(columns, index, label) {
|
|
1098
|
+
return normalizeColumnSelectors(columns).map((requested) => resolveSummaryColumn(requested, index, label));
|
|
1099
|
+
}
|
|
1100
|
+
function resolveSummaryColumn(column, index, label) {
|
|
1101
|
+
const requested = String(column).trim();
|
|
1102
|
+
if (!requested) {
|
|
1103
|
+
throw new Error(`${label} contains an empty column selector`);
|
|
1104
|
+
}
|
|
1105
|
+
if (isNumericKey(requested)) {
|
|
1106
|
+
const hit = index.byId.get(String(Number(requested)));
|
|
1107
|
+
if (!hit) {
|
|
1108
|
+
throw new Error(`${label} references unknown que_id "${requested}"`);
|
|
1109
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
requested,
|
|
1112
|
+
que_id: normalizeQueId(hit.queId),
|
|
1113
|
+
que_title: asNullableString(hit.queTitle),
|
|
1114
|
+
que_type: hit.queType
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
const hit = resolveFieldByKey(requested, index);
|
|
1118
|
+
if (!hit || hit.queId === undefined || hit.queId === null) {
|
|
1119
|
+
throw new Error(`${label} cannot resolve field "${requested}"`);
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
requested,
|
|
1123
|
+
que_id: normalizeQueId(hit.queId),
|
|
1124
|
+
que_title: asNullableString(hit.queTitle),
|
|
1125
|
+
que_type: hit.queType
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
function buildSummaryRow(answers, columns) {
|
|
1129
|
+
const row = {};
|
|
1130
|
+
for (const column of columns) {
|
|
1131
|
+
row[column.requested] = extractSummaryColumnValue(answers, column);
|
|
1132
|
+
}
|
|
1133
|
+
return row;
|
|
1134
|
+
}
|
|
1135
|
+
function extractSummaryColumnValue(answers, column) {
|
|
1136
|
+
const targetId = normalizeColumnSelector(String(column.que_id));
|
|
1137
|
+
const targetTitle = column.que_title ? normalizeColumnSelector(column.que_title) : null;
|
|
1138
|
+
for (const answerRaw of answers) {
|
|
1139
|
+
const answer = asObject(answerRaw);
|
|
1140
|
+
if (!answer) {
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
const answerQueId = asNullableString(answer.queId);
|
|
1144
|
+
if (answerQueId && normalizeColumnSelector(answerQueId) === targetId) {
|
|
1145
|
+
return extractAnswerDisplayValue(answer);
|
|
1146
|
+
}
|
|
1147
|
+
if (targetTitle) {
|
|
1148
|
+
const answerQueTitle = asNullableString(answer.queTitle);
|
|
1149
|
+
if (answerQueTitle && normalizeColumnSelector(answerQueTitle) === targetTitle) {
|
|
1150
|
+
return extractAnswerDisplayValue(answer);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
function extractAnswerDisplayValue(answer) {
|
|
1157
|
+
const tableValues = answer.tableValues ?? answer.table_values;
|
|
1158
|
+
if (tableValues !== undefined) {
|
|
1159
|
+
return tableValues;
|
|
1160
|
+
}
|
|
1161
|
+
const values = asArray(answer.values);
|
|
1162
|
+
if (values.length === 0) {
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
const normalized = values.map((item) => extractAnswerValueCell(item));
|
|
1166
|
+
return normalized.length === 1 ? normalized[0] : normalized;
|
|
1167
|
+
}
|
|
1168
|
+
function extractAnswerValueCell(value) {
|
|
1169
|
+
const obj = asObject(value);
|
|
1170
|
+
if (!obj) {
|
|
1171
|
+
return value;
|
|
1172
|
+
}
|
|
1173
|
+
if (obj.dataValue !== undefined) {
|
|
1174
|
+
return obj.dataValue;
|
|
1175
|
+
}
|
|
1176
|
+
if (obj.value !== undefined) {
|
|
1177
|
+
return obj.value;
|
|
1178
|
+
}
|
|
1179
|
+
if (obj.valueStr !== undefined) {
|
|
1180
|
+
return obj.valueStr;
|
|
1181
|
+
}
|
|
1182
|
+
return obj;
|
|
1183
|
+
}
|
|
1184
|
+
function toFiniteAmount(value) {
|
|
1185
|
+
if (value === null || value === undefined) {
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
if (Array.isArray(value)) {
|
|
1189
|
+
if (value.length !== 1) {
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
return toFiniteAmount(value[0]);
|
|
1193
|
+
}
|
|
1194
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1195
|
+
return value;
|
|
1196
|
+
}
|
|
1197
|
+
if (typeof value === "string") {
|
|
1198
|
+
const normalized = value.replace(/,/g, "").trim();
|
|
1199
|
+
if (!normalized) {
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
const parsed = Number(normalized);
|
|
1203
|
+
if (Number.isFinite(parsed)) {
|
|
1204
|
+
return parsed;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
function toDayBucket(value, timezone) {
|
|
1210
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
1211
|
+
if (first === null || first === undefined) {
|
|
1212
|
+
return "unknown";
|
|
1213
|
+
}
|
|
1214
|
+
if (typeof first === "string") {
|
|
1215
|
+
const trimmed = first.trim();
|
|
1216
|
+
const direct = trimmed.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
1217
|
+
if (direct) {
|
|
1218
|
+
return direct[1];
|
|
1219
|
+
}
|
|
1220
|
+
const parsed = new Date(trimmed);
|
|
1221
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
1222
|
+
return formatDateBucket(parsed, timezone);
|
|
1223
|
+
}
|
|
1224
|
+
return "unknown";
|
|
1225
|
+
}
|
|
1226
|
+
if (typeof first === "number") {
|
|
1227
|
+
const parsed = new Date(first);
|
|
1228
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
1229
|
+
return formatDateBucket(parsed, timezone);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return "unknown";
|
|
1233
|
+
}
|
|
1234
|
+
function formatDateBucket(value, timezone) {
|
|
1235
|
+
try {
|
|
1236
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
1237
|
+
timeZone: timezone,
|
|
1238
|
+
year: "numeric",
|
|
1239
|
+
month: "2-digit",
|
|
1240
|
+
day: "2-digit"
|
|
1241
|
+
}).format(value);
|
|
1242
|
+
}
|
|
1243
|
+
catch {
|
|
1244
|
+
return value.toISOString().slice(0, 10);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
627
1247
|
function buildListPayload(params) {
|
|
628
1248
|
const payload = {
|
|
629
1249
|
pageNum: params.pageNum,
|
|
@@ -1084,7 +1704,7 @@ function errorResult(error) {
|
|
|
1084
1704
|
const payload = toErrorPayload(error);
|
|
1085
1705
|
return {
|
|
1086
1706
|
isError: true,
|
|
1087
|
-
|
|
1707
|
+
// Keep error payload in text to avoid outputSchema(success) validation conflicts across MCP clients.
|
|
1088
1708
|
content: [
|
|
1089
1709
|
{
|
|
1090
1710
|
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.7",
|
|
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"
|