qingflow-mcp 0.3.23 → 0.3.24
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 +33 -5
- package/dist/server.js +614 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ This MCP server wraps Qingflow OpenAPI for:
|
|
|
13
13
|
- `qf_form_get`
|
|
14
14
|
- `qf_field_resolve`
|
|
15
15
|
- `qf_query_plan`
|
|
16
|
+
- `qf_write_plan`
|
|
16
17
|
- `qf_records_list`
|
|
17
18
|
- `qf_record_get`
|
|
18
19
|
- `qf_records_batch_get`
|
|
@@ -117,7 +118,7 @@ npm i -g git+https://github.com/853046310/qingflow-mcp.git
|
|
|
117
118
|
Install from npm (pinned version):
|
|
118
119
|
|
|
119
120
|
```bash
|
|
120
|
-
npm i -g qingflow-mcp@0.3.
|
|
121
|
+
npm i -g qingflow-mcp@0.3.24
|
|
121
122
|
```
|
|
122
123
|
|
|
123
124
|
Or one-click installer:
|
|
@@ -154,8 +155,9 @@ MCP client config example:
|
|
|
154
155
|
|
|
155
156
|
1. `qf_apps_list` to pick app.
|
|
156
157
|
2. `qf_form_get` to inspect field ids/titles and `field_summaries[].write_format`.
|
|
157
|
-
3. `
|
|
158
|
-
4.
|
|
158
|
+
3. For complex forms, run `qf_write_plan` first to surface static blockers and linked-field risks.
|
|
159
|
+
4. `qf_record_create` or `qf_record_update`.
|
|
160
|
+
5. If create/update returns only `request_id`, call `qf_operation_get` to resolve async result.
|
|
159
161
|
|
|
160
162
|
Directory / org flow:
|
|
161
163
|
|
|
@@ -182,7 +184,7 @@ Full calling contract (Chinese):
|
|
|
182
184
|
|
|
183
185
|
## Write Format Discovery
|
|
184
186
|
|
|
185
|
-
For create/update, `0.3.
|
|
187
|
+
For create/update, `0.3.24` now makes special write formats explicit:
|
|
186
188
|
|
|
187
189
|
1. `qf_form_get`
|
|
188
190
|
- `field_summaries[].write_format` is populated for member/department fields.
|
|
@@ -190,6 +192,10 @@ For create/update, `0.3.23` now makes special write formats explicit:
|
|
|
190
192
|
- `qf_record_create` / `qf_record_update` include member/department examples in `limits.special_field_write_formats`.
|
|
191
193
|
3. `qf_record_create` / `qf_record_update`
|
|
192
194
|
- invalid member/department values fail fast with `FIELD_VALUE_FORMAT_ERROR`.
|
|
195
|
+
4. `qf_write_plan`
|
|
196
|
+
- performs static preflight only
|
|
197
|
+
- detects obvious missing required fields, option-linked fields, readonly/system field writes
|
|
198
|
+
- warns when `questionRelations` means final submit may still fail
|
|
193
199
|
|
|
194
200
|
Examples:
|
|
195
201
|
|
|
@@ -217,6 +223,28 @@ Invalid examples that will now fail:
|
|
|
217
223
|
}
|
|
218
224
|
```
|
|
219
225
|
|
|
226
|
+
`qf_write_plan` example:
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"app_key": "21b3d559",
|
|
231
|
+
"operation": "create",
|
|
232
|
+
"fields": {
|
|
233
|
+
"客户名称": "测试客户",
|
|
234
|
+
"归属销售": [{ "userId": "u_123", "userName": "张三" }],
|
|
235
|
+
"报销类型": [{ "optionId": 1, "value": "出差" }]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Use it when:
|
|
241
|
+
|
|
242
|
+
1. the form has conditional required fields
|
|
243
|
+
2. selected options may reveal linked fields
|
|
244
|
+
3. you need a static blocker list before actual submit
|
|
245
|
+
|
|
246
|
+
Do not treat `qf_write_plan` as a guarantee of successful submit. It is a static preflight built from OpenAPI-visible form metadata, not the full Qingflow frontend validation engine.
|
|
247
|
+
|
|
220
248
|
## Unified Query (`qf_query`)
|
|
221
249
|
|
|
222
250
|
`qf_query` is the recommended read entry for agents.
|
|
@@ -466,7 +494,7 @@ If you see runtime errors around `Headers` or missing web APIs:
|
|
|
466
494
|
2. Upgrade package to latest:
|
|
467
495
|
|
|
468
496
|
```bash
|
|
469
|
-
npm i -g qingflow-mcp@0.3.
|
|
497
|
+
npm i -g qingflow-mcp@0.3.24
|
|
470
498
|
```
|
|
471
499
|
|
|
472
500
|
3. Verify runtime:
|
package/dist/server.js
CHANGED
|
@@ -68,7 +68,7 @@ const REQUEST_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_REQUEST_TIMEOUT_MS
|
|
|
68
68
|
const EXECUTION_BUDGET_MS = toPositiveInt(process.env.QINGFLOW_EXECUTION_BUDGET_MS) ?? 20000;
|
|
69
69
|
const WAIT_RESULT_DEFAULT_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_WAIT_RESULT_TIMEOUT_MS) ?? 5000;
|
|
70
70
|
const WAIT_RESULT_POLL_INTERVAL_MS = toPositiveInt(process.env.QINGFLOW_WAIT_RESULT_POLL_INTERVAL_MS) ?? 500;
|
|
71
|
-
const SERVER_VERSION = "0.3.
|
|
71
|
+
const SERVER_VERSION = "0.3.24";
|
|
72
72
|
const MEMBER_QUE_TYPE_KEYWORDS = ["member", "user", "成员", "人员"];
|
|
73
73
|
const DEPARTMENT_QUE_TYPE_KEYWORDS = ["department", "dept", "部门"];
|
|
74
74
|
const accessToken = process.env.QINGFLOW_ACCESS_TOKEN;
|
|
@@ -1345,6 +1345,108 @@ const queryPlanOutputSchema = z.object({
|
|
|
1345
1345
|
generated_at: z.string()
|
|
1346
1346
|
})
|
|
1347
1347
|
});
|
|
1348
|
+
const writePlanInputPublicSchema = z.object({
|
|
1349
|
+
operation: z.enum(["create", "update"]).optional(),
|
|
1350
|
+
app_key: publicStringSchema,
|
|
1351
|
+
apply_id: publicFieldSelectorSchema.optional(),
|
|
1352
|
+
user_id: publicStringSchema.optional(),
|
|
1353
|
+
force_refresh_form: z.boolean().optional(),
|
|
1354
|
+
answers: z.array(publicAnswerInputSchema).optional(),
|
|
1355
|
+
fields: z.record(z.unknown()).optional()
|
|
1356
|
+
});
|
|
1357
|
+
const writePlanInputSchema = z.preprocess(normalizeWritePlanInput, z
|
|
1358
|
+
.object({
|
|
1359
|
+
operation: z.enum(["create", "update"]).optional(),
|
|
1360
|
+
app_key: z.string().min(1),
|
|
1361
|
+
apply_id: z.union([z.string().min(1), z.number().int()]).optional(),
|
|
1362
|
+
user_id: z.string().min(1).optional(),
|
|
1363
|
+
force_refresh_form: z.boolean().optional(),
|
|
1364
|
+
answers: z.array(answerInputSchema).optional(),
|
|
1365
|
+
fields: z.record(fieldValueSchema).optional()
|
|
1366
|
+
})
|
|
1367
|
+
.refine((value) => hasWritePayload(value.answers, value.fields), {
|
|
1368
|
+
message: "Either answers or fields is required"
|
|
1369
|
+
}));
|
|
1370
|
+
const writePlanFieldRefSchema = z.object({
|
|
1371
|
+
source: z.enum(["fields", "answers"]),
|
|
1372
|
+
requested: z.string(),
|
|
1373
|
+
resolved: z.boolean(),
|
|
1374
|
+
que_id: z.union([z.string(), z.number(), z.null()]),
|
|
1375
|
+
que_title: z.string().nullable(),
|
|
1376
|
+
que_type: z.unknown().nullable(),
|
|
1377
|
+
required: z.boolean().nullable(),
|
|
1378
|
+
readonly: z.boolean().nullable(),
|
|
1379
|
+
system: z.boolean().nullable(),
|
|
1380
|
+
write_format: fieldSummarySchema.shape.write_format,
|
|
1381
|
+
reason: z.string().nullable()
|
|
1382
|
+
});
|
|
1383
|
+
const writePlanFieldIssueSchema = z.object({
|
|
1384
|
+
que_id: z.union([z.string(), z.number(), z.null()]),
|
|
1385
|
+
que_title: z.string().nullable(),
|
|
1386
|
+
que_type: z.unknown().nullable(),
|
|
1387
|
+
reason: z.string()
|
|
1388
|
+
});
|
|
1389
|
+
const writePlanInvalidFieldSchema = z.object({
|
|
1390
|
+
location: z.string().nullable(),
|
|
1391
|
+
message: z.string(),
|
|
1392
|
+
error_code: z.string().nullable(),
|
|
1393
|
+
field: z
|
|
1394
|
+
.object({
|
|
1395
|
+
que_id: z.union([z.string(), z.number(), z.null()]),
|
|
1396
|
+
que_title: z.string().nullable(),
|
|
1397
|
+
que_type: z.unknown().nullable()
|
|
1398
|
+
})
|
|
1399
|
+
.nullable(),
|
|
1400
|
+
expected_format: fieldSummarySchema.shape.write_format,
|
|
1401
|
+
received_value: z.unknown().optional()
|
|
1402
|
+
});
|
|
1403
|
+
const writePlanOptionLinkSchema = z.object({
|
|
1404
|
+
source_que_id: z.union([z.string(), z.number(), z.null()]),
|
|
1405
|
+
source_que_title: z.string().nullable(),
|
|
1406
|
+
option_id: z.union([z.string(), z.number(), z.null()]),
|
|
1407
|
+
option_value: z.string().nullable(),
|
|
1408
|
+
linked_fields: z.array(z.object({
|
|
1409
|
+
que_id: z.union([z.string(), z.number(), z.null()]),
|
|
1410
|
+
que_title: z.string().nullable(),
|
|
1411
|
+
required: z.boolean().nullable(),
|
|
1412
|
+
provided: z.boolean()
|
|
1413
|
+
})),
|
|
1414
|
+
missing_linked_fields: z.array(z.object({
|
|
1415
|
+
que_id: z.union([z.string(), z.number(), z.null()]),
|
|
1416
|
+
que_title: z.string().nullable(),
|
|
1417
|
+
required: z.boolean().nullable()
|
|
1418
|
+
}))
|
|
1419
|
+
});
|
|
1420
|
+
const writePlanOutputSchema = z.object({
|
|
1421
|
+
ok: z.literal(true),
|
|
1422
|
+
data: z.object({
|
|
1423
|
+
operation: z.enum(["create", "update"]),
|
|
1424
|
+
app_key: z.string(),
|
|
1425
|
+
apply_id: z.union([z.string(), z.number(), z.null()]),
|
|
1426
|
+
normalized_answers: z.array(z.record(z.unknown())),
|
|
1427
|
+
resolved_fields: z.array(writePlanFieldRefSchema),
|
|
1428
|
+
validation: z.object({
|
|
1429
|
+
valid: z.boolean(),
|
|
1430
|
+
missing_required_fields: z.array(writePlanFieldIssueSchema),
|
|
1431
|
+
likely_hidden_required_fields: z.array(writePlanFieldIssueSchema),
|
|
1432
|
+
readonly_or_system_fields: z.array(writePlanFieldIssueSchema),
|
|
1433
|
+
invalid_fields: z.array(writePlanInvalidFieldSchema),
|
|
1434
|
+
warnings: z.array(z.string())
|
|
1435
|
+
}),
|
|
1436
|
+
dependencies: z.object({
|
|
1437
|
+
question_relations_present: z.boolean(),
|
|
1438
|
+
relation_count: z.number().int().nonnegative(),
|
|
1439
|
+
option_links: z.array(writePlanOptionLinkSchema)
|
|
1440
|
+
}),
|
|
1441
|
+
ready_to_submit: z.boolean(),
|
|
1442
|
+
blockers: z.array(z.string()),
|
|
1443
|
+
recommended_next_actions: z.array(z.string())
|
|
1444
|
+
}),
|
|
1445
|
+
meta: z.object({
|
|
1446
|
+
version: z.string(),
|
|
1447
|
+
generated_at: z.string()
|
|
1448
|
+
})
|
|
1449
|
+
});
|
|
1348
1450
|
const batchGetInputPublicSchema = z
|
|
1349
1451
|
.object({
|
|
1350
1452
|
app_key: publicStringSchema,
|
|
@@ -2188,6 +2290,25 @@ server.registerTool("qf_query_plan", {
|
|
|
2188
2290
|
return errorResult(error);
|
|
2189
2291
|
}
|
|
2190
2292
|
});
|
|
2293
|
+
server.registerTool("qf_write_plan", {
|
|
2294
|
+
title: "Qingflow Write Plan",
|
|
2295
|
+
description: "Static preflight for create/update payloads. Resolves fields, normalizes answers, checks obvious required/dependency risks, and warns when questionRelations make runtime validation uncertain.",
|
|
2296
|
+
inputSchema: writePlanInputPublicSchema,
|
|
2297
|
+
outputSchema: writePlanOutputSchema,
|
|
2298
|
+
annotations: {
|
|
2299
|
+
readOnlyHint: true,
|
|
2300
|
+
idempotentHint: true
|
|
2301
|
+
}
|
|
2302
|
+
}, async (args) => {
|
|
2303
|
+
try {
|
|
2304
|
+
const parsedArgs = writePlanInputSchema.parse(args);
|
|
2305
|
+
const payload = await executeWritePlan(parsedArgs);
|
|
2306
|
+
return okResult(payload, `Planned ${payload.data.operation} for ${parsedArgs.app_key}`);
|
|
2307
|
+
}
|
|
2308
|
+
catch (error) {
|
|
2309
|
+
return errorResult(error);
|
|
2310
|
+
}
|
|
2311
|
+
});
|
|
2191
2312
|
server.registerTool("qf_records_list", {
|
|
2192
2313
|
title: "Qingflow Records List",
|
|
2193
2314
|
description: "List records with pagination, filters and sorting.",
|
|
@@ -3590,6 +3711,28 @@ function buildToolSpecCatalog() {
|
|
|
3590
3711
|
resolve_fields: true
|
|
3591
3712
|
}
|
|
3592
3713
|
},
|
|
3714
|
+
{
|
|
3715
|
+
tool: "qf_write_plan",
|
|
3716
|
+
required: ["app_key", "answers or fields"],
|
|
3717
|
+
limits: {
|
|
3718
|
+
operation: "create|update (default create; auto-switches to update when apply_id is present)",
|
|
3719
|
+
input_contract: "strict JSON only; answers must be array and fields must be object",
|
|
3720
|
+
capability_boundary: "Static preflight only. Can detect obvious required/dependency risks, but cannot fully execute runtime visibility/validation rules from Qingflow frontend.",
|
|
3721
|
+
special_field_write_formats: {
|
|
3722
|
+
member_list: [{ userId: "u_123", userName: "张三" }],
|
|
3723
|
+
department_list: [{ deptId: 111, deptName: "销售部" }]
|
|
3724
|
+
}
|
|
3725
|
+
},
|
|
3726
|
+
aliases: {},
|
|
3727
|
+
minimal_example: {
|
|
3728
|
+
app_key: "21b3d559",
|
|
3729
|
+
operation: "create",
|
|
3730
|
+
fields: {
|
|
3731
|
+
客户名称: "测试客户",
|
|
3732
|
+
归属销售: [{ userId: "u_123", userName: "张三" }]
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
},
|
|
3593
3736
|
{
|
|
3594
3737
|
tool: "qf_records_list",
|
|
3595
3738
|
required: ["app_key", "select_columns"],
|
|
@@ -3993,6 +4136,26 @@ function normalizeQueryPlanInput(raw) {
|
|
|
3993
4136
|
probe: coerceBooleanLike(normalizedObj.probe)
|
|
3994
4137
|
};
|
|
3995
4138
|
}
|
|
4139
|
+
function normalizeWritePlanInput(raw) {
|
|
4140
|
+
const parsedRoot = parseJsonLikeDeep(raw);
|
|
4141
|
+
const obj = asObject(parsedRoot);
|
|
4142
|
+
if (!obj) {
|
|
4143
|
+
return parsedRoot;
|
|
4144
|
+
}
|
|
4145
|
+
const normalizedObj = applyAliases(obj, {
|
|
4146
|
+
...COMMON_INPUT_ALIASES,
|
|
4147
|
+
applyId: "apply_id",
|
|
4148
|
+
forceRefreshForm: "force_refresh_form",
|
|
4149
|
+
mode: "operation",
|
|
4150
|
+
action: "operation"
|
|
4151
|
+
});
|
|
4152
|
+
return {
|
|
4153
|
+
...normalizedObj,
|
|
4154
|
+
apply_id: coerceNumberLike(normalizeSelectorInputValue(normalizedObj.apply_id)) ?? coerceStringLike(normalizedObj.apply_id),
|
|
4155
|
+
user_id: coerceStringLike(normalizedObj.user_id),
|
|
4156
|
+
force_refresh_form: coerceBooleanLike(normalizedObj.force_refresh_form)
|
|
4157
|
+
};
|
|
4158
|
+
}
|
|
3996
4159
|
function normalizeBatchGetInput(raw) {
|
|
3997
4160
|
const parsedRoot = parseJsonLikeDeep(raw);
|
|
3998
4161
|
const obj = asObject(parsedRoot);
|
|
@@ -5717,6 +5880,456 @@ async function executeQueryPlan(args) {
|
|
|
5717
5880
|
}
|
|
5718
5881
|
};
|
|
5719
5882
|
}
|
|
5883
|
+
async function executeWritePlan(args) {
|
|
5884
|
+
const operation = args.operation ?? (args.apply_id !== undefined ? "update" : "create");
|
|
5885
|
+
const form = await getFormCached(args.app_key, args.user_id, Boolean(args.force_refresh_form));
|
|
5886
|
+
const formObj = asObject(form.result);
|
|
5887
|
+
const index = buildFieldIndex(form.result);
|
|
5888
|
+
const fields = Array.from(index.byId.values());
|
|
5889
|
+
const resolvedFields = collectWritePlanFieldRefs({
|
|
5890
|
+
fieldsInput: args.fields,
|
|
5891
|
+
answersInput: args.answers,
|
|
5892
|
+
index
|
|
5893
|
+
});
|
|
5894
|
+
const invalidFields = [];
|
|
5895
|
+
const warnings = [];
|
|
5896
|
+
let normalizedAnswers = [];
|
|
5897
|
+
try {
|
|
5898
|
+
normalizedAnswers = resolveAnswers({
|
|
5899
|
+
explicitAnswers: args.answers,
|
|
5900
|
+
fields: args.fields,
|
|
5901
|
+
form: form.result,
|
|
5902
|
+
tool: "qf_write_plan"
|
|
5903
|
+
});
|
|
5904
|
+
}
|
|
5905
|
+
catch (error) {
|
|
5906
|
+
invalidFields.push(...normalizeWritePlanErrors(error));
|
|
5907
|
+
}
|
|
5908
|
+
const providedFieldIds = new Set();
|
|
5909
|
+
for (const ref of resolvedFields) {
|
|
5910
|
+
if (ref.resolved && ref.que_id !== null) {
|
|
5911
|
+
providedFieldIds.add(String(ref.que_id));
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
for (const answer of normalizedAnswers) {
|
|
5915
|
+
const queId = answer.queId;
|
|
5916
|
+
if (queId !== undefined && queId !== null) {
|
|
5917
|
+
providedFieldIds.add(String(normalizeQueId(queId)));
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5920
|
+
const questionRelations = asArray(formObj?.questionRelations);
|
|
5921
|
+
const relationReferencedFieldIds = extractRelationReferencedFieldIds(questionRelations);
|
|
5922
|
+
const optionLinks = collectWritePlanOptionLinks({
|
|
5923
|
+
normalizedAnswers,
|
|
5924
|
+
index,
|
|
5925
|
+
providedFieldIds
|
|
5926
|
+
});
|
|
5927
|
+
const optionLinkedMissingIds = new Set();
|
|
5928
|
+
for (const item of optionLinks) {
|
|
5929
|
+
for (const linked of item.missing_linked_fields) {
|
|
5930
|
+
if (linked.que_id !== null) {
|
|
5931
|
+
optionLinkedMissingIds.add(String(linked.que_id));
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5935
|
+
const missingRequiredFields = [];
|
|
5936
|
+
const likelyHiddenRequiredFields = [];
|
|
5937
|
+
const readonlyOrSystemFields = [];
|
|
5938
|
+
for (const field of fields) {
|
|
5939
|
+
if (field.queId === undefined || field.queId === null) {
|
|
5940
|
+
continue;
|
|
5941
|
+
}
|
|
5942
|
+
const queId = normalizeQueId(field.queId);
|
|
5943
|
+
const required = extractFieldRequiredFlag(field);
|
|
5944
|
+
const readonly = extractFieldReadonlyFlag(field);
|
|
5945
|
+
const system = extractFieldSystemFlag(field);
|
|
5946
|
+
const provided = providedFieldIds.has(String(queId));
|
|
5947
|
+
if (provided && (readonly === true || system === true)) {
|
|
5948
|
+
readonlyOrSystemFields.push({
|
|
5949
|
+
que_id: queId,
|
|
5950
|
+
que_title: asNullableString(field.queTitle),
|
|
5951
|
+
que_type: field.queType ?? null,
|
|
5952
|
+
reason: readonly === true && system === true
|
|
5953
|
+
? "field looks readonly and system-managed"
|
|
5954
|
+
: readonly === true
|
|
5955
|
+
? "field looks readonly"
|
|
5956
|
+
: "field looks system-managed"
|
|
5957
|
+
});
|
|
5958
|
+
}
|
|
5959
|
+
if (provided || required !== true) {
|
|
5960
|
+
continue;
|
|
5961
|
+
}
|
|
5962
|
+
const issue = {
|
|
5963
|
+
que_id: queId,
|
|
5964
|
+
que_title: asNullableString(field.queTitle),
|
|
5965
|
+
que_type: field.queType ?? null,
|
|
5966
|
+
reason: optionLinkedMissingIds.has(String(queId))
|
|
5967
|
+
? "required field is linked by a selected option but not provided"
|
|
5968
|
+
: relationReferencedFieldIds.has(String(queId))
|
|
5969
|
+
? "required field participates in questionRelations; it may be runtime-hidden/linked"
|
|
5970
|
+
: "required field not provided"
|
|
5971
|
+
};
|
|
5972
|
+
if (optionLinkedMissingIds.has(String(queId)) ||
|
|
5973
|
+
relationReferencedFieldIds.has(String(queId))) {
|
|
5974
|
+
likelyHiddenRequiredFields.push(issue);
|
|
5975
|
+
}
|
|
5976
|
+
else {
|
|
5977
|
+
missingRequiredFields.push(issue);
|
|
5978
|
+
}
|
|
5979
|
+
}
|
|
5980
|
+
if (questionRelations.length > 0) {
|
|
5981
|
+
warnings.push(`form contains ${questionRelations.length} questionRelations; qf_write_plan can only provide static preflight and cannot fully evaluate runtime visibility/required rules`);
|
|
5982
|
+
}
|
|
5983
|
+
if (optionLinks.some((item) => item.missing_linked_fields.length > 0)) {
|
|
5984
|
+
warnings.push("selected options link to additional fields that are not yet provided");
|
|
5985
|
+
}
|
|
5986
|
+
if (normalizedAnswers.length === 0 && invalidFields.length === 0) {
|
|
5987
|
+
warnings.push("no normalized answers were produced from the current payload");
|
|
5988
|
+
}
|
|
5989
|
+
const blockers = [];
|
|
5990
|
+
if (invalidFields.length > 0) {
|
|
5991
|
+
blockers.push(`invalid fields: ${invalidFields.map((item) => item.message).join("; ")}`);
|
|
5992
|
+
}
|
|
5993
|
+
if (missingRequiredFields.length > 0) {
|
|
5994
|
+
blockers.push(`missing required fields: ${missingRequiredFields
|
|
5995
|
+
.map((item) => item.que_title ?? String(item.que_id ?? "unknown"))
|
|
5996
|
+
.join(", ")}`);
|
|
5997
|
+
}
|
|
5998
|
+
if (likelyHiddenRequiredFields.length > 0) {
|
|
5999
|
+
blockers.push(`linked or runtime-dependent required fields may still be missing: ${likelyHiddenRequiredFields
|
|
6000
|
+
.map((item) => item.que_title ?? String(item.que_id ?? "unknown"))
|
|
6001
|
+
.join(", ")}`);
|
|
6002
|
+
}
|
|
6003
|
+
if (readonlyOrSystemFields.length > 0) {
|
|
6004
|
+
blockers.push(`payload includes readonly/system fields: ${readonlyOrSystemFields
|
|
6005
|
+
.map((item) => item.que_title ?? String(item.que_id ?? "unknown"))
|
|
6006
|
+
.join(", ")}`);
|
|
6007
|
+
}
|
|
6008
|
+
const recommendedNextActions = [
|
|
6009
|
+
"Use qf_form_get to inspect field_summaries and write_format before final submit."
|
|
6010
|
+
];
|
|
6011
|
+
if (missingRequiredFields.length > 0 || likelyHiddenRequiredFields.length > 0) {
|
|
6012
|
+
recommendedNextActions.push("Fill missing required fields before calling qf_record_create or qf_record_update.");
|
|
6013
|
+
}
|
|
6014
|
+
if (optionLinks.some((item) => item.missing_linked_fields.length > 0)) {
|
|
6015
|
+
recommendedNextActions.push("Provide fields linked by the currently selected options, or change the option values.");
|
|
6016
|
+
}
|
|
6017
|
+
if (invalidFields.some((item) => item.expected_format?.kind === "member_list")) {
|
|
6018
|
+
recommendedNextActions.push("Use qf_users_list or qf_department_users_list to resolve valid userId values.");
|
|
6019
|
+
}
|
|
6020
|
+
if (invalidFields.some((item) => item.expected_format?.kind === "department_list")) {
|
|
6021
|
+
recommendedNextActions.push("Use qf_departments_list to resolve valid deptId values.");
|
|
6022
|
+
}
|
|
6023
|
+
if (questionRelations.length > 0) {
|
|
6024
|
+
recommendedNextActions.push("Even when ready_to_submit=true, final submit can still fail because runtime questionRelations are not fully evaluable via OpenAPI.");
|
|
6025
|
+
}
|
|
6026
|
+
return {
|
|
6027
|
+
ok: true,
|
|
6028
|
+
data: {
|
|
6029
|
+
operation,
|
|
6030
|
+
app_key: args.app_key,
|
|
6031
|
+
apply_id: args.apply_id ?? null,
|
|
6032
|
+
normalized_answers: normalizedAnswers,
|
|
6033
|
+
resolved_fields: resolvedFields,
|
|
6034
|
+
validation: {
|
|
6035
|
+
valid: invalidFields.length === 0 &&
|
|
6036
|
+
missingRequiredFields.length === 0 &&
|
|
6037
|
+
likelyHiddenRequiredFields.length === 0 &&
|
|
6038
|
+
readonlyOrSystemFields.length === 0,
|
|
6039
|
+
missing_required_fields: missingRequiredFields,
|
|
6040
|
+
likely_hidden_required_fields: likelyHiddenRequiredFields,
|
|
6041
|
+
readonly_or_system_fields: readonlyOrSystemFields,
|
|
6042
|
+
invalid_fields: invalidFields,
|
|
6043
|
+
warnings: uniqueStringList(warnings)
|
|
6044
|
+
},
|
|
6045
|
+
dependencies: {
|
|
6046
|
+
question_relations_present: questionRelations.length > 0,
|
|
6047
|
+
relation_count: questionRelations.length,
|
|
6048
|
+
option_links: optionLinks
|
|
6049
|
+
},
|
|
6050
|
+
ready_to_submit: invalidFields.length === 0 &&
|
|
6051
|
+
missingRequiredFields.length === 0 &&
|
|
6052
|
+
likelyHiddenRequiredFields.length === 0 &&
|
|
6053
|
+
readonlyOrSystemFields.length === 0,
|
|
6054
|
+
blockers: uniqueStringList(blockers),
|
|
6055
|
+
recommended_next_actions: uniqueStringList(recommendedNextActions)
|
|
6056
|
+
},
|
|
6057
|
+
meta: {
|
|
6058
|
+
version: SERVER_VERSION,
|
|
6059
|
+
generated_at: new Date().toISOString()
|
|
6060
|
+
}
|
|
6061
|
+
};
|
|
6062
|
+
}
|
|
6063
|
+
function collectWritePlanFieldRefs(params) {
|
|
6064
|
+
const refs = [];
|
|
6065
|
+
for (const fieldKey of Object.keys(params.fieldsInput ?? {})) {
|
|
6066
|
+
refs.push(resolveWritePlanFieldRef({
|
|
6067
|
+
source: "fields",
|
|
6068
|
+
requested: fieldKey,
|
|
6069
|
+
index: params.index
|
|
6070
|
+
}));
|
|
6071
|
+
}
|
|
6072
|
+
for (const item of params.answersInput ?? []) {
|
|
6073
|
+
const requestedRaw = item.que_id ?? item.queId ?? item.que_title ?? item.queTitle;
|
|
6074
|
+
if (requestedRaw === undefined || requestedRaw === null) {
|
|
6075
|
+
continue;
|
|
6076
|
+
}
|
|
6077
|
+
refs.push(resolveWritePlanFieldRef({
|
|
6078
|
+
source: "answers",
|
|
6079
|
+
requested: String(requestedRaw),
|
|
6080
|
+
index: params.index
|
|
6081
|
+
}));
|
|
6082
|
+
}
|
|
6083
|
+
return refs;
|
|
6084
|
+
}
|
|
6085
|
+
function resolveWritePlanFieldRef(params) {
|
|
6086
|
+
const requested = params.requested.trim();
|
|
6087
|
+
let field = null;
|
|
6088
|
+
let reason = null;
|
|
6089
|
+
if (requested) {
|
|
6090
|
+
if (isNumericKey(requested)) {
|
|
6091
|
+
field = params.index.byId.get(String(Number(requested))) ?? null;
|
|
6092
|
+
if (!field) {
|
|
6093
|
+
reason = "field not found in form metadata";
|
|
6094
|
+
}
|
|
6095
|
+
}
|
|
6096
|
+
else {
|
|
6097
|
+
try {
|
|
6098
|
+
field = resolveFieldSelectorStrict({
|
|
6099
|
+
fieldKey: requested,
|
|
6100
|
+
index: params.index,
|
|
6101
|
+
tool: "qf_write_plan",
|
|
6102
|
+
location: `${params.source}.${requested}`
|
|
6103
|
+
});
|
|
6104
|
+
}
|
|
6105
|
+
catch (error) {
|
|
6106
|
+
reason = error instanceof Error ? error.message : String(error);
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
return {
|
|
6111
|
+
source: params.source,
|
|
6112
|
+
requested,
|
|
6113
|
+
resolved: Boolean(field && field.queId !== undefined && field.queId !== null),
|
|
6114
|
+
que_id: field?.queId !== undefined && field.queId !== null ? normalizeQueId(field.queId) : null,
|
|
6115
|
+
que_title: asNullableString(field?.queTitle),
|
|
6116
|
+
que_type: field?.queType ?? null,
|
|
6117
|
+
required: field ? extractFieldRequiredFlag(field) : null,
|
|
6118
|
+
readonly: field ? extractFieldReadonlyFlag(field) : null,
|
|
6119
|
+
system: field ? extractFieldSystemFlag(field) : null,
|
|
6120
|
+
write_format: field ? inferFieldWriteFormat(field) : null,
|
|
6121
|
+
reason
|
|
6122
|
+
};
|
|
6123
|
+
}
|
|
6124
|
+
function normalizeWritePlanErrors(error) {
|
|
6125
|
+
if (error instanceof InputValidationError) {
|
|
6126
|
+
const details = asObject(error.details);
|
|
6127
|
+
const field = asObject(details?.field);
|
|
6128
|
+
return [
|
|
6129
|
+
{
|
|
6130
|
+
location: asNullableString(details?.location),
|
|
6131
|
+
message: error.message,
|
|
6132
|
+
error_code: error.errorCode,
|
|
6133
|
+
field: field
|
|
6134
|
+
? {
|
|
6135
|
+
que_id: field.que_id ?? null,
|
|
6136
|
+
que_title: asNullableString(field.que_title),
|
|
6137
|
+
que_type: field.que_type ?? null
|
|
6138
|
+
}
|
|
6139
|
+
: null,
|
|
6140
|
+
expected_format: details?.expected_format ?? null,
|
|
6141
|
+
received_value: details?.received_value
|
|
6142
|
+
}
|
|
6143
|
+
];
|
|
6144
|
+
}
|
|
6145
|
+
if (error instanceof z.ZodError) {
|
|
6146
|
+
return error.issues.map((issue) => ({
|
|
6147
|
+
location: issue.path.length > 0 ? issue.path.join(".") : null,
|
|
6148
|
+
message: issue.message,
|
|
6149
|
+
error_code: "INVALID_ARGUMENTS",
|
|
6150
|
+
field: null,
|
|
6151
|
+
expected_format: null,
|
|
6152
|
+
received_value: undefined
|
|
6153
|
+
}));
|
|
6154
|
+
}
|
|
6155
|
+
return [
|
|
6156
|
+
{
|
|
6157
|
+
location: null,
|
|
6158
|
+
message: error instanceof Error ? error.message : String(error),
|
|
6159
|
+
error_code: "WRITE_PLAN_ERROR",
|
|
6160
|
+
field: null,
|
|
6161
|
+
expected_format: null,
|
|
6162
|
+
received_value: undefined
|
|
6163
|
+
}
|
|
6164
|
+
];
|
|
6165
|
+
}
|
|
6166
|
+
function collectWritePlanOptionLinks(params) {
|
|
6167
|
+
const links = [];
|
|
6168
|
+
for (const answer of params.normalizedAnswers) {
|
|
6169
|
+
const queId = answer.queId;
|
|
6170
|
+
if (queId === undefined || queId === null) {
|
|
6171
|
+
continue;
|
|
6172
|
+
}
|
|
6173
|
+
const field = params.index.byId.get(String(normalizeQueId(queId)));
|
|
6174
|
+
const fieldObj = asObject(field);
|
|
6175
|
+
const options = asArray(fieldObj?.options);
|
|
6176
|
+
if (options.length === 0) {
|
|
6177
|
+
continue;
|
|
6178
|
+
}
|
|
6179
|
+
const selected = collectSelectedOptionKeys(asArray(answer.values));
|
|
6180
|
+
for (const optionRaw of options) {
|
|
6181
|
+
const option = asObject(optionRaw);
|
|
6182
|
+
if (!option) {
|
|
6183
|
+
continue;
|
|
6184
|
+
}
|
|
6185
|
+
const optionIdRaw = option.optId ?? option.optionId ?? option.id ?? null;
|
|
6186
|
+
const optionId = typeof optionIdRaw === "number" && Number.isFinite(optionIdRaw)
|
|
6187
|
+
? Math.trunc(optionIdRaw)
|
|
6188
|
+
: (asNullableString(optionIdRaw) ?? null);
|
|
6189
|
+
const optionValue = asNullableString(option.optValue ?? option.value ?? option.label ?? option.name);
|
|
6190
|
+
const isSelected = (optionId !== null && selected.ids.has(String(optionId))) ||
|
|
6191
|
+
(optionValue !== null && selected.values.has(optionValue.trim().toLowerCase()));
|
|
6192
|
+
if (!isSelected) {
|
|
6193
|
+
continue;
|
|
6194
|
+
}
|
|
6195
|
+
const linkedFieldIds = uniqueStringList(asArray(option.linkQueIds)
|
|
6196
|
+
.map((item) => typeof item === "number"
|
|
6197
|
+
? String(Math.trunc(item))
|
|
6198
|
+
: (asNullableString(item)?.trim() ?? ""))
|
|
6199
|
+
.filter((item) => item.length > 0));
|
|
6200
|
+
if (linkedFieldIds.length === 0) {
|
|
6201
|
+
continue;
|
|
6202
|
+
}
|
|
6203
|
+
const linkedFields = linkedFieldIds.map((fieldId) => {
|
|
6204
|
+
const linked = params.index.byId.get(String(Number(fieldId))) ?? params.index.byId.get(fieldId) ?? null;
|
|
6205
|
+
return {
|
|
6206
|
+
que_id: linked?.queId !== undefined && linked.queId !== null
|
|
6207
|
+
? normalizeQueId(linked.queId)
|
|
6208
|
+
: (isNumericKey(fieldId) ? Number(fieldId) : fieldId),
|
|
6209
|
+
que_title: asNullableString(linked?.queTitle),
|
|
6210
|
+
required: linked ? extractFieldRequiredFlag(linked) : null,
|
|
6211
|
+
provided: params.providedFieldIds.has(fieldId) || params.providedFieldIds.has(String(Number(fieldId)))
|
|
6212
|
+
};
|
|
6213
|
+
});
|
|
6214
|
+
links.push({
|
|
6215
|
+
source_que_id: normalizeQueId(queId),
|
|
6216
|
+
source_que_title: asNullableString(field?.queTitle),
|
|
6217
|
+
option_id: optionId ?? null,
|
|
6218
|
+
option_value: optionValue,
|
|
6219
|
+
linked_fields: linkedFields,
|
|
6220
|
+
missing_linked_fields: linkedFields
|
|
6221
|
+
.filter((item) => !item.provided)
|
|
6222
|
+
.map((item) => ({
|
|
6223
|
+
que_id: item.que_id,
|
|
6224
|
+
que_title: item.que_title,
|
|
6225
|
+
required: item.required
|
|
6226
|
+
}))
|
|
6227
|
+
});
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
return links;
|
|
6231
|
+
}
|
|
6232
|
+
function collectSelectedOptionKeys(values) {
|
|
6233
|
+
const ids = new Set();
|
|
6234
|
+
const texts = new Set();
|
|
6235
|
+
for (const value of values) {
|
|
6236
|
+
if (value === null || value === undefined) {
|
|
6237
|
+
continue;
|
|
6238
|
+
}
|
|
6239
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
6240
|
+
ids.add(String(value));
|
|
6241
|
+
texts.add(String(value).trim().toLowerCase());
|
|
6242
|
+
continue;
|
|
6243
|
+
}
|
|
6244
|
+
const obj = asObject(value);
|
|
6245
|
+
if (!obj) {
|
|
6246
|
+
continue;
|
|
6247
|
+
}
|
|
6248
|
+
for (const candidate of [obj.optionId, obj.optId, obj.id]) {
|
|
6249
|
+
if (candidate === undefined || candidate === null) {
|
|
6250
|
+
continue;
|
|
6251
|
+
}
|
|
6252
|
+
ids.add(String(candidate));
|
|
6253
|
+
}
|
|
6254
|
+
for (const candidate of [obj.value, obj.dataValue, obj.valueStr, obj.label, obj.name]) {
|
|
6255
|
+
const normalized = asNullableString(candidate)?.trim().toLowerCase();
|
|
6256
|
+
if (normalized) {
|
|
6257
|
+
texts.add(normalized);
|
|
6258
|
+
}
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
return { ids, values: texts };
|
|
6262
|
+
}
|
|
6263
|
+
function extractRelationReferencedFieldIds(relations) {
|
|
6264
|
+
const ids = new Set();
|
|
6265
|
+
const visit = (value, keyHint = "") => {
|
|
6266
|
+
if (value === null || value === undefined) {
|
|
6267
|
+
return;
|
|
6268
|
+
}
|
|
6269
|
+
if (Array.isArray(value)) {
|
|
6270
|
+
for (const item of value) {
|
|
6271
|
+
visit(item, keyHint);
|
|
6272
|
+
}
|
|
6273
|
+
return;
|
|
6274
|
+
}
|
|
6275
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
6276
|
+
if (keyHint && /queid|linkqueids/i.test(keyHint)) {
|
|
6277
|
+
ids.add(String(Math.trunc(value)));
|
|
6278
|
+
}
|
|
6279
|
+
return;
|
|
6280
|
+
}
|
|
6281
|
+
if (typeof value === "string") {
|
|
6282
|
+
const trimmed = value.trim();
|
|
6283
|
+
if (trimmed && keyHint && /queid|linkqueids/i.test(keyHint)) {
|
|
6284
|
+
ids.add(isNumericKey(trimmed) ? String(Number(trimmed)) : trimmed);
|
|
6285
|
+
}
|
|
6286
|
+
return;
|
|
6287
|
+
}
|
|
6288
|
+
const obj = asObject(value);
|
|
6289
|
+
if (!obj) {
|
|
6290
|
+
return;
|
|
6291
|
+
}
|
|
6292
|
+
for (const [key, child] of Object.entries(obj)) {
|
|
6293
|
+
visit(child, key);
|
|
6294
|
+
}
|
|
6295
|
+
};
|
|
6296
|
+
visit(relations);
|
|
6297
|
+
return ids;
|
|
6298
|
+
}
|
|
6299
|
+
function extractFieldRequiredFlag(field) {
|
|
6300
|
+
const obj = asObject(field);
|
|
6301
|
+
if (!obj) {
|
|
6302
|
+
return null;
|
|
6303
|
+
}
|
|
6304
|
+
return firstBooleanFromKeys(obj, ["required", "isRequired", "verifyRequired", "mustFill", "must_fill"]);
|
|
6305
|
+
}
|
|
6306
|
+
function extractFieldReadonlyFlag(field) {
|
|
6307
|
+
const obj = asObject(field);
|
|
6308
|
+
if (!obj) {
|
|
6309
|
+
return null;
|
|
6310
|
+
}
|
|
6311
|
+
const direct = firstBooleanFromKeys(obj, ["readonly", "readOnly", "isReadonly", "isReadOnly"]);
|
|
6312
|
+
if (direct !== null) {
|
|
6313
|
+
return direct;
|
|
6314
|
+
}
|
|
6315
|
+
const editable = firstBooleanFromKeys(obj, ["editable", "isEditable"]);
|
|
6316
|
+
return editable === null ? null : !editable;
|
|
6317
|
+
}
|
|
6318
|
+
function extractFieldSystemFlag(field) {
|
|
6319
|
+
const obj = asObject(field);
|
|
6320
|
+
if (!obj) {
|
|
6321
|
+
return null;
|
|
6322
|
+
}
|
|
6323
|
+
return firstBooleanFromKeys(obj, ["system", "isSystem", "systemField", "isSystemField"]);
|
|
6324
|
+
}
|
|
6325
|
+
function firstBooleanFromKeys(obj, keys) {
|
|
6326
|
+
for (const key of keys) {
|
|
6327
|
+
if (typeof obj[key] === "boolean") {
|
|
6328
|
+
return obj[key];
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
6331
|
+
return null;
|
|
6332
|
+
}
|
|
5720
6333
|
async function executeRecordsBatchGet(args) {
|
|
5721
6334
|
if (!args.app_key) {
|
|
5722
6335
|
throw missingRequiredFieldError({
|