qingflow-mcp 0.3.22 → 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 +70 -5
- package/dist/server.js +888 -24
- 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:
|
|
@@ -153,9 +154,10 @@ MCP client config example:
|
|
|
153
154
|
## Recommended Flow
|
|
154
155
|
|
|
155
156
|
1. `qf_apps_list` to pick app.
|
|
156
|
-
2. `qf_form_get` to inspect field ids/titles.
|
|
157
|
-
3. `
|
|
158
|
-
4.
|
|
157
|
+
2. `qf_form_get` to inspect field ids/titles and `field_summaries[].write_format`.
|
|
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
|
|
|
@@ -180,6 +182,69 @@ Full calling contract (Chinese):
|
|
|
180
182
|
|
|
181
183
|
- [MCP 调用规范](./docs/MCP_CALLING_SPEC.md)
|
|
182
184
|
|
|
185
|
+
## Write Format Discovery
|
|
186
|
+
|
|
187
|
+
For create/update, `0.3.24` now makes special write formats explicit:
|
|
188
|
+
|
|
189
|
+
1. `qf_form_get`
|
|
190
|
+
- `field_summaries[].write_format` is populated for member/department fields.
|
|
191
|
+
2. `qf_tool_spec_get`
|
|
192
|
+
- `qf_record_create` / `qf_record_update` include member/department examples in `limits.special_field_write_formats`.
|
|
193
|
+
3. `qf_record_create` / `qf_record_update`
|
|
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
|
|
199
|
+
|
|
200
|
+
Examples:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"fields": {
|
|
205
|
+
"归属销售": [
|
|
206
|
+
{ "userId": "u_123", "userName": "张三" }
|
|
207
|
+
],
|
|
208
|
+
"归属部门": [
|
|
209
|
+
{ "deptId": 111, "deptName": "销售部" }
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Invalid examples that will now fail:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"fields": {
|
|
220
|
+
"归属销售": "张三",
|
|
221
|
+
"归属部门": "销售部"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
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
|
+
|
|
183
248
|
## Unified Query (`qf_query`)
|
|
184
249
|
|
|
185
250
|
`qf_query` is the recommended read entry for agents.
|
|
@@ -429,7 +494,7 @@ If you see runtime errors around `Headers` or missing web APIs:
|
|
|
429
494
|
2. Upgrade package to latest:
|
|
430
495
|
|
|
431
496
|
```bash
|
|
432
|
-
npm i -g qingflow-mcp@
|
|
497
|
+
npm i -g qingflow-mcp@0.3.24
|
|
433
498
|
```
|
|
434
499
|
|
|
435
500
|
3. Verify runtime:
|
package/dist/server.js
CHANGED
|
@@ -68,7 +68,9 @@ 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
|
+
const MEMBER_QUE_TYPE_KEYWORDS = ["member", "user", "成员", "人员"];
|
|
73
|
+
const DEPARTMENT_QUE_TYPE_KEYWORDS = ["department", "dept", "部门"];
|
|
72
74
|
const accessToken = process.env.QINGFLOW_ACCESS_TOKEN;
|
|
73
75
|
const baseUrl = process.env.QINGFLOW_BASE_URL;
|
|
74
76
|
if (!accessToken) {
|
|
@@ -192,6 +194,15 @@ const fieldSummarySchema = z.object({
|
|
|
192
194
|
que_id: z.union([z.number(), z.string(), z.null()]),
|
|
193
195
|
que_title: z.string().nullable(),
|
|
194
196
|
que_type: z.unknown(),
|
|
197
|
+
write_format: z
|
|
198
|
+
.object({
|
|
199
|
+
kind: z.enum(["member_list", "department_list"]),
|
|
200
|
+
description: z.string(),
|
|
201
|
+
item_shape: z.record(z.string()),
|
|
202
|
+
example: z.array(z.record(z.unknown())),
|
|
203
|
+
resolution_hint: z.string()
|
|
204
|
+
})
|
|
205
|
+
.nullable(),
|
|
195
206
|
has_sub_fields: z.boolean(),
|
|
196
207
|
sub_field_count: z.number().int().nonnegative()
|
|
197
208
|
});
|
|
@@ -1334,6 +1345,108 @@ const queryPlanOutputSchema = z.object({
|
|
|
1334
1345
|
generated_at: z.string()
|
|
1335
1346
|
})
|
|
1336
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
|
+
});
|
|
1337
1450
|
const batchGetInputPublicSchema = z
|
|
1338
1451
|
.object({
|
|
1339
1452
|
app_key: publicStringSchema,
|
|
@@ -2177,6 +2290,25 @@ server.registerTool("qf_query_plan", {
|
|
|
2177
2290
|
return errorResult(error);
|
|
2178
2291
|
}
|
|
2179
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
|
+
});
|
|
2180
2312
|
server.registerTool("qf_records_list", {
|
|
2181
2313
|
title: "Qingflow Records List",
|
|
2182
2314
|
description: "List records with pagination, filters and sorting.",
|
|
@@ -2388,7 +2520,8 @@ server.registerTool("qf_record_create", {
|
|
|
2388
2520
|
}, async (args) => {
|
|
2389
2521
|
try {
|
|
2390
2522
|
const parsedArgs = createInputSchema.parse(args);
|
|
2391
|
-
const
|
|
2523
|
+
const shouldFetchForm = hasWritePayload(parsedArgs.answers, parsedArgs.fields) || Boolean(parsedArgs.force_refresh_form);
|
|
2524
|
+
const form = shouldFetchForm
|
|
2392
2525
|
? await getFormCached(parsedArgs.app_key, parsedArgs.user_id, Boolean(parsedArgs.force_refresh_form))
|
|
2393
2526
|
: null;
|
|
2394
2527
|
const normalizedAnswers = resolveAnswers({
|
|
@@ -2464,16 +2597,18 @@ server.registerTool("qf_record_update", {
|
|
|
2464
2597
|
}, async (args) => {
|
|
2465
2598
|
try {
|
|
2466
2599
|
const parsedArgs = updateInputSchema.parse(args);
|
|
2467
|
-
const
|
|
2468
|
-
|
|
2600
|
+
const resolvedAppKey = parsedArgs.app_key ?? getCachedApplyAppKey(parsedArgs.apply_id);
|
|
2601
|
+
const requiresFormByTitle = needsFormResolution(parsedArgs.fields);
|
|
2602
|
+
if (requiresFormByTitle && !resolvedAppKey) {
|
|
2469
2603
|
throw missingRequiredFieldError({
|
|
2470
2604
|
field: "app_key",
|
|
2471
2605
|
tool: "qf_record_update",
|
|
2472
2606
|
fixHint: "Provide app_key when fields uses title-based keys, or switch fields to numeric que_id."
|
|
2473
2607
|
});
|
|
2474
2608
|
}
|
|
2475
|
-
const form =
|
|
2476
|
-
|
|
2609
|
+
const form = (hasWritePayload(parsedArgs.answers, parsedArgs.fields) || Boolean(parsedArgs.force_refresh_form)) &&
|
|
2610
|
+
resolvedAppKey
|
|
2611
|
+
? await getFormCached(resolvedAppKey, parsedArgs.user_id, Boolean(parsedArgs.force_refresh_form))
|
|
2477
2612
|
: null;
|
|
2478
2613
|
const normalizedAnswers = resolveAnswers({
|
|
2479
2614
|
explicitAnswers: parsedArgs.answers,
|
|
@@ -3534,7 +3669,8 @@ function buildToolSpecCatalog() {
|
|
|
3534
3669
|
tool: "qf_form_get",
|
|
3535
3670
|
required: ["app_key"],
|
|
3536
3671
|
limits: {
|
|
3537
|
-
app_key: "required string"
|
|
3672
|
+
app_key: "required string",
|
|
3673
|
+
write_format_hint: "field_summaries[].write_format is populated for member/department fields so agents can discover exact create/update payload shapes before writing."
|
|
3538
3674
|
},
|
|
3539
3675
|
aliases: collectAliasHints(["app_key", "user_id", "force_refresh"], {}),
|
|
3540
3676
|
minimal_example: {
|
|
@@ -3575,6 +3711,28 @@ function buildToolSpecCatalog() {
|
|
|
3575
3711
|
resolve_fields: true
|
|
3576
3712
|
}
|
|
3577
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
|
+
},
|
|
3578
3736
|
{
|
|
3579
3737
|
tool: "qf_records_list",
|
|
3580
3738
|
required: ["app_key", "select_columns"],
|
|
@@ -3751,7 +3909,11 @@ function buildToolSpecCatalog() {
|
|
|
3751
3909
|
write_mode: "Provide either answers[] or fields{}",
|
|
3752
3910
|
wait_result: "optional boolean; when true, polls qf_operation_get internally and returns resolved apply_id directly",
|
|
3753
3911
|
wait_timeout_ms: "optional int (max 20000); default 5000ms",
|
|
3754
|
-
input_contract: "strict JSON only; answers must be array and fields must be object"
|
|
3912
|
+
input_contract: "strict JSON only; answers must be array and fields must be object",
|
|
3913
|
+
special_field_write_formats: {
|
|
3914
|
+
member_list: [{ userId: "u_123", userName: "张三" }],
|
|
3915
|
+
department_list: [{ deptId: 111, deptName: "销售部" }]
|
|
3916
|
+
}
|
|
3755
3917
|
},
|
|
3756
3918
|
aliases: {},
|
|
3757
3919
|
minimal_example: {
|
|
@@ -3769,7 +3931,11 @@ function buildToolSpecCatalog() {
|
|
|
3769
3931
|
write_mode: "Provide either answers[] or fields{}",
|
|
3770
3932
|
wait_result: "optional boolean; when true, polls qf_operation_get internally and returns resolved result directly",
|
|
3771
3933
|
wait_timeout_ms: "optional int (max 20000); default 5000ms",
|
|
3772
|
-
input_contract: "strict JSON only; answers must be array and fields must be object"
|
|
3934
|
+
input_contract: "strict JSON only; answers must be array and fields must be object",
|
|
3935
|
+
special_field_write_formats: {
|
|
3936
|
+
member_list: [{ userId: "u_123", userName: "张三" }],
|
|
3937
|
+
department_list: [{ deptId: 111, deptName: "销售部" }]
|
|
3938
|
+
}
|
|
3773
3939
|
},
|
|
3774
3940
|
aliases: {},
|
|
3775
3941
|
minimal_example: {
|
|
@@ -3970,6 +4136,26 @@ function normalizeQueryPlanInput(raw) {
|
|
|
3970
4136
|
probe: coerceBooleanLike(normalizedObj.probe)
|
|
3971
4137
|
};
|
|
3972
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
|
+
}
|
|
3973
4159
|
function normalizeBatchGetInput(raw) {
|
|
3974
4160
|
const parsedRoot = parseJsonLikeDeep(raw);
|
|
3975
4161
|
const obj = asObject(parsedRoot);
|
|
@@ -5694,6 +5880,456 @@ async function executeQueryPlan(args) {
|
|
|
5694
5880
|
}
|
|
5695
5881
|
};
|
|
5696
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
|
+
}
|
|
5697
6333
|
async function executeRecordsBatchGet(args) {
|
|
5698
6334
|
if (!args.app_key) {
|
|
5699
6335
|
throw missingRequiredFieldError({
|
|
@@ -7533,8 +8169,9 @@ function normalizeRecordItem(raw, includeAnswers) {
|
|
|
7533
8169
|
return normalized;
|
|
7534
8170
|
}
|
|
7535
8171
|
function resolveAnswers(params) {
|
|
7536
|
-
const
|
|
7537
|
-
const
|
|
8172
|
+
const index = params.form ? buildFieldIndex(params.form) : null;
|
|
8173
|
+
const normalizedFromFields = resolveFieldAnswers(params.fields, index, params.tool);
|
|
8174
|
+
const normalizedExplicit = normalizeExplicitAnswers(params.explicitAnswers, index, params.tool);
|
|
7538
8175
|
const merged = new Map();
|
|
7539
8176
|
for (const answer of normalizedFromFields) {
|
|
7540
8177
|
merged.set(String(answer.queId), answer);
|
|
@@ -7547,12 +8184,12 @@ function resolveAnswers(params) {
|
|
|
7547
8184
|
}
|
|
7548
8185
|
return Array.from(merged.values());
|
|
7549
8186
|
}
|
|
7550
|
-
function normalizeExplicitAnswers(answers) {
|
|
8187
|
+
function normalizeExplicitAnswers(answers, index, tool = "qf_record_create") {
|
|
7551
8188
|
if (!answers?.length) {
|
|
7552
8189
|
return [];
|
|
7553
8190
|
}
|
|
7554
8191
|
const output = [];
|
|
7555
|
-
for (const item of answers) {
|
|
8192
|
+
for (const [itemIndex, item] of answers.entries()) {
|
|
7556
8193
|
const queId = item.que_id ?? item.queId;
|
|
7557
8194
|
if (queId === undefined || queId === null || String(queId).trim() === "") {
|
|
7558
8195
|
throw new Error("answer item requires que_id or queId");
|
|
@@ -7578,27 +8215,32 @@ function normalizeExplicitAnswers(answers) {
|
|
|
7578
8215
|
if (values === undefined) {
|
|
7579
8216
|
throw new Error(`answer item ${String(queId)} requires values or table_values`);
|
|
7580
8217
|
}
|
|
7581
|
-
|
|
8218
|
+
const field = resolveExplicitAnswerField(item, index);
|
|
8219
|
+
normalized.values = values.map((value, valueIndex) => normalizeAnswerValue(value, {
|
|
8220
|
+
field,
|
|
8221
|
+
tool,
|
|
8222
|
+
location: `answers[${itemIndex}].values[${valueIndex}]`
|
|
8223
|
+
}));
|
|
7582
8224
|
output.push(normalized);
|
|
7583
8225
|
}
|
|
7584
8226
|
return output;
|
|
7585
8227
|
}
|
|
7586
|
-
function resolveFieldAnswers(fields,
|
|
8228
|
+
function resolveFieldAnswers(fields, index, tool = "qf_record_create") {
|
|
7587
8229
|
const entries = Object.entries(fields ?? {});
|
|
7588
8230
|
if (entries.length === 0) {
|
|
7589
8231
|
return [];
|
|
7590
8232
|
}
|
|
7591
|
-
const
|
|
8233
|
+
const resolvedIndex = index ?? { byId: new Map(), byTitle: new Map() };
|
|
7592
8234
|
const answers = [];
|
|
7593
8235
|
for (const [fieldKey, fieldValue] of entries) {
|
|
7594
8236
|
let field;
|
|
7595
8237
|
if (isNumericKey(fieldKey)) {
|
|
7596
|
-
field = resolveFieldByKey(fieldKey,
|
|
8238
|
+
field = resolveFieldByKey(fieldKey, resolvedIndex);
|
|
7597
8239
|
}
|
|
7598
8240
|
else {
|
|
7599
8241
|
field = resolveFieldSelectorStrict({
|
|
7600
8242
|
fieldKey,
|
|
7601
|
-
index,
|
|
8243
|
+
index: resolvedIndex,
|
|
7602
8244
|
tool,
|
|
7603
8245
|
location: `fields.${fieldKey}`
|
|
7604
8246
|
});
|
|
@@ -7612,15 +8254,15 @@ function resolveFieldAnswers(fields, form, tool = "qf_record_create") {
|
|
|
7612
8254
|
tool,
|
|
7613
8255
|
location: `fields.${fieldKey}`,
|
|
7614
8256
|
requested: fieldKey,
|
|
7615
|
-
suggestions: buildFieldSuggestions(fieldKey,
|
|
8257
|
+
suggestions: buildFieldSuggestions(fieldKey, resolvedIndex)
|
|
7616
8258
|
}
|
|
7617
8259
|
});
|
|
7618
8260
|
}
|
|
7619
|
-
answers.push(makeAnswerFromField(field, fieldValue));
|
|
8261
|
+
answers.push(makeAnswerFromField(field, fieldValue, tool));
|
|
7620
8262
|
}
|
|
7621
8263
|
return answers;
|
|
7622
8264
|
}
|
|
7623
|
-
function makeAnswerFromField(field, value) {
|
|
8265
|
+
function makeAnswerFromField(field, value, tool = "qf_record_create") {
|
|
7624
8266
|
const base = {
|
|
7625
8267
|
queId: field.queId
|
|
7626
8268
|
};
|
|
@@ -7641,7 +8283,11 @@ function makeAnswerFromField(field, value) {
|
|
|
7641
8283
|
if ("values" in objectValue) {
|
|
7642
8284
|
return {
|
|
7643
8285
|
...base,
|
|
7644
|
-
values: asArray(objectValue.values).map((item) => normalizeAnswerValue(item
|
|
8286
|
+
values: asArray(objectValue.values).map((item, index) => normalizeAnswerValue(item, {
|
|
8287
|
+
field,
|
|
8288
|
+
tool,
|
|
8289
|
+
location: `fields.${String(field.queTitle ?? field.queId ?? "field")}.values[${index}]`
|
|
8290
|
+
}))
|
|
7645
8291
|
};
|
|
7646
8292
|
}
|
|
7647
8293
|
}
|
|
@@ -7654,10 +8300,38 @@ function makeAnswerFromField(field, value) {
|
|
|
7654
8300
|
const valueArray = Array.isArray(value) ? value : [value];
|
|
7655
8301
|
return {
|
|
7656
8302
|
...base,
|
|
7657
|
-
values: valueArray.map((item) => normalizeAnswerValue(item
|
|
8303
|
+
values: valueArray.map((item, index) => normalizeAnswerValue(item, {
|
|
8304
|
+
field,
|
|
8305
|
+
tool,
|
|
8306
|
+
location: `fields.${String(field.queTitle ?? field.queId ?? "field")}.values[${index}]`
|
|
8307
|
+
}))
|
|
7658
8308
|
};
|
|
7659
8309
|
}
|
|
7660
|
-
function
|
|
8310
|
+
function resolveExplicitAnswerField(item, index) {
|
|
8311
|
+
if (index) {
|
|
8312
|
+
const rawQueId = item.que_id ?? item.queId;
|
|
8313
|
+
if (rawQueId !== undefined && rawQueId !== null) {
|
|
8314
|
+
const hit = index.byId.get(String(normalizeQueId(rawQueId)));
|
|
8315
|
+
if (hit) {
|
|
8316
|
+
return hit;
|
|
8317
|
+
}
|
|
8318
|
+
}
|
|
8319
|
+
const rawQueTitle = item.que_title ?? item.queTitle;
|
|
8320
|
+
if (typeof rawQueTitle === "string" && rawQueTitle.trim()) {
|
|
8321
|
+
const candidates = index.byTitle.get(rawQueTitle.trim().toLowerCase()) ?? [];
|
|
8322
|
+
if (candidates.length === 1) {
|
|
8323
|
+
return candidates[0];
|
|
8324
|
+
}
|
|
8325
|
+
}
|
|
8326
|
+
}
|
|
8327
|
+
return {
|
|
8328
|
+
queId: item.que_id ?? item.queId,
|
|
8329
|
+
queTitle: item.que_title ?? item.queTitle,
|
|
8330
|
+
queType: item.que_type ?? item.queType
|
|
8331
|
+
};
|
|
8332
|
+
}
|
|
8333
|
+
function normalizeAnswerValue(value, params) {
|
|
8334
|
+
validateWriteValueShape(params?.field ?? null, value, params);
|
|
7661
8335
|
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
7662
8336
|
return {
|
|
7663
8337
|
value,
|
|
@@ -7666,6 +8340,70 @@ function normalizeAnswerValue(value) {
|
|
|
7666
8340
|
}
|
|
7667
8341
|
return value;
|
|
7668
8342
|
}
|
|
8343
|
+
function validateWriteValueShape(field, value, params) {
|
|
8344
|
+
const writeFormat = field ? inferFieldWriteFormat(field) : null;
|
|
8345
|
+
if (!writeFormat) {
|
|
8346
|
+
return;
|
|
8347
|
+
}
|
|
8348
|
+
const validatedField = field;
|
|
8349
|
+
if (!validatedField) {
|
|
8350
|
+
return;
|
|
8351
|
+
}
|
|
8352
|
+
if (writeFormat.kind === "member_list" && !isValidMemberWriteValue(value)) {
|
|
8353
|
+
throw fieldValueFormatError({
|
|
8354
|
+
field: validatedField,
|
|
8355
|
+
writeFormat,
|
|
8356
|
+
value,
|
|
8357
|
+
tool: params?.tool,
|
|
8358
|
+
location: params?.location
|
|
8359
|
+
});
|
|
8360
|
+
}
|
|
8361
|
+
if (writeFormat.kind === "department_list" && !isValidDepartmentWriteValue(value)) {
|
|
8362
|
+
throw fieldValueFormatError({
|
|
8363
|
+
field: validatedField,
|
|
8364
|
+
writeFormat,
|
|
8365
|
+
value,
|
|
8366
|
+
tool: params?.tool,
|
|
8367
|
+
location: params?.location
|
|
8368
|
+
});
|
|
8369
|
+
}
|
|
8370
|
+
}
|
|
8371
|
+
function isValidMemberWriteValue(value) {
|
|
8372
|
+
const obj = asObject(value);
|
|
8373
|
+
return Boolean(obj && asNullableString(obj.userId)?.trim());
|
|
8374
|
+
}
|
|
8375
|
+
function isValidDepartmentWriteValue(value) {
|
|
8376
|
+
const obj = asObject(value);
|
|
8377
|
+
if (!obj) {
|
|
8378
|
+
return false;
|
|
8379
|
+
}
|
|
8380
|
+
const deptId = obj.deptId;
|
|
8381
|
+
return ((typeof deptId === "string" && deptId.trim().length > 0) ||
|
|
8382
|
+
(typeof deptId === "number" && Number.isFinite(deptId)));
|
|
8383
|
+
}
|
|
8384
|
+
function fieldValueFormatError(params) {
|
|
8385
|
+
const fieldLabel = asNullableString(params.field.queTitle)?.trim() ||
|
|
8386
|
+
String(params.field.queId ?? "unknown_field");
|
|
8387
|
+
const formatName = params.writeFormat.kind === "member_list" ? "成员字段" : "部门字段";
|
|
8388
|
+
return new InputValidationError({
|
|
8389
|
+
message: `${formatName} "${fieldLabel}" 的写入值格式不正确`,
|
|
8390
|
+
errorCode: "FIELD_VALUE_FORMAT_ERROR",
|
|
8391
|
+
fixHint: params.writeFormat.kind === "member_list"
|
|
8392
|
+
? '传对象数组,例如 [{"userId":"u_123","userName":"张三"}];不要传纯字符串或 user_id。'
|
|
8393
|
+
: '传对象数组,例如 [{"deptId":111,"deptName":"销售部"}];不要传纯字符串或 dept_id。',
|
|
8394
|
+
details: {
|
|
8395
|
+
tool: params.tool ?? "qf_record_create",
|
|
8396
|
+
location: params.location ?? null,
|
|
8397
|
+
field: {
|
|
8398
|
+
que_id: params.field.queId ?? null,
|
|
8399
|
+
que_title: asNullableString(params.field.queTitle),
|
|
8400
|
+
que_type: params.field.queType ?? null
|
|
8401
|
+
},
|
|
8402
|
+
expected_format: params.writeFormat,
|
|
8403
|
+
received_value: params.value
|
|
8404
|
+
}
|
|
8405
|
+
});
|
|
8406
|
+
}
|
|
7669
8407
|
function needsFormResolution(fields) {
|
|
7670
8408
|
const keys = Object.keys(fields ?? {});
|
|
7671
8409
|
if (!keys.length) {
|
|
@@ -7697,11 +8435,95 @@ function extractFieldSummaries(form) {
|
|
|
7697
8435
|
que_id: field.queId ?? null,
|
|
7698
8436
|
que_title: asNullableString(field.queTitle),
|
|
7699
8437
|
que_type: field.queType,
|
|
8438
|
+
write_format: inferFieldWriteFormat(field),
|
|
7700
8439
|
has_sub_fields: sub.length > 0,
|
|
7701
8440
|
sub_field_count: sub.length
|
|
7702
8441
|
};
|
|
7703
8442
|
});
|
|
7704
8443
|
}
|
|
8444
|
+
function inferFieldWriteFormat(field) {
|
|
8445
|
+
const kind = inferFieldWriteFormatKind(field.queType);
|
|
8446
|
+
if (kind === "member_list") {
|
|
8447
|
+
return {
|
|
8448
|
+
kind,
|
|
8449
|
+
description: "Pass one or more member objects in values[]. Each item must contain userId.",
|
|
8450
|
+
item_shape: {
|
|
8451
|
+
userId: "string",
|
|
8452
|
+
userName: "string? (recommended)",
|
|
8453
|
+
name: "string? (accepted alias)"
|
|
8454
|
+
},
|
|
8455
|
+
example: [
|
|
8456
|
+
{
|
|
8457
|
+
userId: "u_123",
|
|
8458
|
+
userName: "张三"
|
|
8459
|
+
}
|
|
8460
|
+
],
|
|
8461
|
+
resolution_hint: "Use qf_users_list or qf_department_users_list to resolve valid userId values before writing."
|
|
8462
|
+
};
|
|
8463
|
+
}
|
|
8464
|
+
if (kind === "department_list") {
|
|
8465
|
+
return {
|
|
8466
|
+
kind,
|
|
8467
|
+
description: "Pass one or more department objects in values[]. Each item must contain deptId.",
|
|
8468
|
+
item_shape: {
|
|
8469
|
+
deptId: "string|number",
|
|
8470
|
+
deptName: "string? (recommended)",
|
|
8471
|
+
name: "string? (accepted alias)"
|
|
8472
|
+
},
|
|
8473
|
+
example: [
|
|
8474
|
+
{
|
|
8475
|
+
deptId: 111,
|
|
8476
|
+
deptName: "销售部"
|
|
8477
|
+
}
|
|
8478
|
+
],
|
|
8479
|
+
resolution_hint: "Use qf_departments_list to resolve valid deptId values before writing."
|
|
8480
|
+
};
|
|
8481
|
+
}
|
|
8482
|
+
return null;
|
|
8483
|
+
}
|
|
8484
|
+
function inferFieldWriteFormatKind(queType) {
|
|
8485
|
+
const tokens = collectQueTypeTokens(queType);
|
|
8486
|
+
if (tokens.some((token) => MEMBER_QUE_TYPE_KEYWORDS.some((keyword) => token.includes(keyword)))) {
|
|
8487
|
+
return "member_list";
|
|
8488
|
+
}
|
|
8489
|
+
if (tokens.some((token) => DEPARTMENT_QUE_TYPE_KEYWORDS.some((keyword) => token.includes(keyword)))) {
|
|
8490
|
+
return "department_list";
|
|
8491
|
+
}
|
|
8492
|
+
return null;
|
|
8493
|
+
}
|
|
8494
|
+
function collectQueTypeTokens(queType) {
|
|
8495
|
+
const tokens = new Set();
|
|
8496
|
+
const queue = [queType];
|
|
8497
|
+
while (queue.length > 0) {
|
|
8498
|
+
const current = queue.shift();
|
|
8499
|
+
if (current === null || current === undefined) {
|
|
8500
|
+
continue;
|
|
8501
|
+
}
|
|
8502
|
+
if (typeof current === "string") {
|
|
8503
|
+
const normalized = current.trim().toLowerCase();
|
|
8504
|
+
if (normalized) {
|
|
8505
|
+
tokens.add(normalized);
|
|
8506
|
+
}
|
|
8507
|
+
continue;
|
|
8508
|
+
}
|
|
8509
|
+
if (typeof current === "number" || typeof current === "boolean") {
|
|
8510
|
+
tokens.add(String(current));
|
|
8511
|
+
continue;
|
|
8512
|
+
}
|
|
8513
|
+
if (Array.isArray(current)) {
|
|
8514
|
+
queue.push(...current);
|
|
8515
|
+
continue;
|
|
8516
|
+
}
|
|
8517
|
+
const obj = asObject(current);
|
|
8518
|
+
if (!obj) {
|
|
8519
|
+
continue;
|
|
8520
|
+
}
|
|
8521
|
+
for (const value of Object.values(obj)) {
|
|
8522
|
+
queue.push(value);
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8525
|
+
return Array.from(tokens);
|
|
8526
|
+
}
|
|
7705
8527
|
function buildFieldIndex(form) {
|
|
7706
8528
|
const byId = new Map();
|
|
7707
8529
|
const byTitle = new Map();
|
|
@@ -8442,6 +9264,48 @@ function buildErrorExampleCalls(params) {
|
|
|
8442
9264
|
}
|
|
8443
9265
|
];
|
|
8444
9266
|
}
|
|
9267
|
+
if (errorCode === "FIELD_VALUE_FORMAT_ERROR") {
|
|
9268
|
+
const expectedFormat = asObject(params.details?.expected_format);
|
|
9269
|
+
const kind = asNullableString(expectedFormat?.kind);
|
|
9270
|
+
if (kind === "member_list") {
|
|
9271
|
+
return [
|
|
9272
|
+
{
|
|
9273
|
+
tool: "qf_form_get",
|
|
9274
|
+
arguments: {
|
|
9275
|
+
app_key: appKey
|
|
9276
|
+
},
|
|
9277
|
+
note: "先查看字段 write_format,确认成员字段写入 shape"
|
|
9278
|
+
},
|
|
9279
|
+
{
|
|
9280
|
+
tool: "qf_department_users_list",
|
|
9281
|
+
arguments: {
|
|
9282
|
+
dept_id: 111,
|
|
9283
|
+
fetch_child: false
|
|
9284
|
+
},
|
|
9285
|
+
note: "查询有效成员 userId,再按 [{userId,userName}] 写入"
|
|
9286
|
+
}
|
|
9287
|
+
];
|
|
9288
|
+
}
|
|
9289
|
+
if (kind === "department_list") {
|
|
9290
|
+
return [
|
|
9291
|
+
{
|
|
9292
|
+
tool: "qf_form_get",
|
|
9293
|
+
arguments: {
|
|
9294
|
+
app_key: appKey
|
|
9295
|
+
},
|
|
9296
|
+
note: "先查看字段 write_format,确认部门字段写入 shape"
|
|
9297
|
+
},
|
|
9298
|
+
{
|
|
9299
|
+
tool: "qf_departments_list",
|
|
9300
|
+
arguments: {
|
|
9301
|
+
keyword: "销售",
|
|
9302
|
+
limit: 20
|
|
9303
|
+
},
|
|
9304
|
+
note: "查询有效部门 deptId,再按 [{deptId,deptName}] 写入"
|
|
9305
|
+
}
|
|
9306
|
+
];
|
|
9307
|
+
}
|
|
9308
|
+
}
|
|
8445
9309
|
if (errorCode === "INTERNAL_ERROR" || errorCode === "UNKNOWN_ERROR") {
|
|
8446
9310
|
return [
|
|
8447
9311
|
{
|