@josephyan/qingflow-app-user-mcp 0.2.0-beta.21 → 0.2.0-beta.22
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 +4 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-user/SKILL.md +35 -202
- package/skills/qingflow-app-user/agents/openai.yaml +2 -2
- package/skills/qingflow-app-user/references/data-gotchas.md +3 -2
- package/skills/qingflow-app-user/references/record-patterns.md +7 -9
- package/skills/qingflow-record-analysis/SKILL.md +11 -4
- package/skills/qingflow-record-analysis/references/analysis-gotchas.md +1 -1
- package/skills/qingflow-record-analysis/references/analysis-patterns.md +15 -5
- package/skills/qingflow-record-analysis/references/confidence-reporting.md +3 -3
- package/skills/qingflow-record-crud/SKILL.md +173 -0
- package/skills/qingflow-record-crud/agents/openai.yaml +5 -0
- package/skills/qingflow-record-crud/references/data-gotchas.md +43 -0
- package/skills/qingflow-record-crud/references/environments.md +58 -0
- package/skills/qingflow-record-crud/references/record-patterns.md +109 -0
- package/skills/qingflow-task-ops/SKILL.md +148 -0
- package/skills/qingflow-task-ops/agents/openai.yaml +4 -0
- package/skills/qingflow-task-ops/references/environments.md +44 -0
- package/skills/qingflow-task-ops/references/workflow-usage.md +27 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/server.py +3 -2
- package/src/qingflow_mcp/server_app_user.py +1 -1
- package/src/qingflow_mcp/tools/record_tools.py +422 -263
|
@@ -243,7 +243,8 @@ class RecordTools(ToolBase):
|
|
|
243
243
|
@mcp.tool(
|
|
244
244
|
description=(
|
|
245
245
|
"Write Qingflow records with a SQL-like JSON DSL. "
|
|
246
|
-
"Use record_schema_get first, then choose operation=insert|update|delete
|
|
246
|
+
"Use record_schema_get first, then choose operation=insert|update|delete. "
|
|
247
|
+
"This tool performs internal preflight validation before any write is applied. "
|
|
247
248
|
"This route does not accept raw SQL strings or free-form WHERE clauses."
|
|
248
249
|
)
|
|
249
250
|
)
|
|
@@ -251,7 +252,6 @@ class RecordTools(ToolBase):
|
|
|
251
252
|
profile: str = DEFAULT_PROFILE,
|
|
252
253
|
app_key: str = "",
|
|
253
254
|
operation: str = "insert",
|
|
254
|
-
mode: str = "plan",
|
|
255
255
|
record_id: int | None = None,
|
|
256
256
|
record_ids: list[int] | None = None,
|
|
257
257
|
values: list[JSONObject] | None = None,
|
|
@@ -264,7 +264,6 @@ class RecordTools(ToolBase):
|
|
|
264
264
|
profile=profile,
|
|
265
265
|
app_key=app_key,
|
|
266
266
|
operation=operation,
|
|
267
|
-
mode=mode,
|
|
268
267
|
record_id=record_id,
|
|
269
268
|
record_ids=record_ids or [],
|
|
270
269
|
values=values or [],
|
|
@@ -564,7 +563,6 @@ class RecordTools(ToolBase):
|
|
|
564
563
|
profile: str,
|
|
565
564
|
app_key: str,
|
|
566
565
|
operation: str,
|
|
567
|
-
mode: str,
|
|
568
566
|
record_id: int | None,
|
|
569
567
|
record_ids: list[int],
|
|
570
568
|
values: list[JSONObject],
|
|
@@ -572,14 +570,18 @@ class RecordTools(ToolBase):
|
|
|
572
570
|
submit_type: str | int,
|
|
573
571
|
verify_write: bool,
|
|
574
572
|
output_profile: str,
|
|
573
|
+
mode: str | None = None,
|
|
575
574
|
) -> JSONObject:
|
|
576
575
|
normalized_output_profile = self._normalize_public_output_profile(output_profile)
|
|
577
576
|
normalized_operation = operation.strip().lower()
|
|
578
|
-
normalized_mode = mode.strip().lower()
|
|
579
577
|
if normalized_operation not in {"insert", "update", "delete"}:
|
|
580
578
|
raise_tool_error(QingflowApiError.config_error("operation must be insert, update, or delete"))
|
|
581
|
-
if
|
|
582
|
-
raise_tool_error(
|
|
579
|
+
if mode is not None:
|
|
580
|
+
raise_tool_error(
|
|
581
|
+
QingflowApiError.config_error(
|
|
582
|
+
"record_write no longer accepts mode; it now runs internal preflight automatically before apply"
|
|
583
|
+
)
|
|
584
|
+
)
|
|
583
585
|
if not app_key:
|
|
584
586
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
585
587
|
normalized_record_ids = [item for item in record_ids if isinstance(item, int) and item > 0]
|
|
@@ -591,34 +593,36 @@ class RecordTools(ToolBase):
|
|
|
591
593
|
if set:
|
|
592
594
|
raise_tool_error(QingflowApiError.config_error("insert must use values, not set"))
|
|
593
595
|
normalized_answers = self._normalize_record_write_clauses(values, location="values")
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
"
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
596
|
+
raw_preflight = self._preflight_record_write(
|
|
597
|
+
profile=profile,
|
|
598
|
+
operation="create",
|
|
599
|
+
app_key=app_key,
|
|
600
|
+
apply_id=None,
|
|
601
|
+
answers=normalized_answers,
|
|
602
|
+
fields={},
|
|
603
|
+
force_refresh_form=False,
|
|
604
|
+
)
|
|
605
|
+
preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
606
|
+
normalized_payload: JSONObject = self._record_write_normalized_payload(
|
|
607
|
+
operation="insert",
|
|
608
|
+
record_id=None,
|
|
609
|
+
record_ids=[],
|
|
610
|
+
normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
|
|
611
|
+
submit_type=submit_type_value,
|
|
612
|
+
)
|
|
613
|
+
if preflight_data.get("blockers"):
|
|
614
|
+
return self._record_write_blocked_response(
|
|
615
|
+
raw_preflight,
|
|
613
616
|
operation="insert",
|
|
614
617
|
normalized_payload=normalized_payload,
|
|
615
618
|
output_profile=normalized_output_profile,
|
|
616
619
|
human_review=False,
|
|
620
|
+
target_resource={"type": "record", "app_key": app_key, "record_id": None, "record_ids": []},
|
|
617
621
|
)
|
|
618
622
|
raw_apply = self.record_create(
|
|
619
623
|
profile=profile,
|
|
620
624
|
app_key=app_key,
|
|
621
|
-
answers=normalized_answers,
|
|
625
|
+
answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
|
|
622
626
|
fields={},
|
|
623
627
|
submit_type=submit_type_value,
|
|
624
628
|
verify_write=verify_write,
|
|
@@ -630,6 +634,7 @@ class RecordTools(ToolBase):
|
|
|
630
634
|
normalized_payload=normalized_payload,
|
|
631
635
|
output_profile=normalized_output_profile,
|
|
632
636
|
human_review=False,
|
|
637
|
+
preflight=raw_preflight,
|
|
633
638
|
)
|
|
634
639
|
|
|
635
640
|
if normalized_operation == "update":
|
|
@@ -640,35 +645,37 @@ class RecordTools(ToolBase):
|
|
|
640
645
|
if values:
|
|
641
646
|
raise_tool_error(QingflowApiError.config_error("update must use set, not values"))
|
|
642
647
|
normalized_answers = self._normalize_record_write_clauses(set, location="set")
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
"
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
648
|
+
raw_preflight = self._preflight_record_write(
|
|
649
|
+
profile=profile,
|
|
650
|
+
operation="update",
|
|
651
|
+
app_key=app_key,
|
|
652
|
+
apply_id=record_id,
|
|
653
|
+
answers=normalized_answers,
|
|
654
|
+
fields={},
|
|
655
|
+
force_refresh_form=False,
|
|
656
|
+
)
|
|
657
|
+
preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
658
|
+
normalized_payload = self._record_write_normalized_payload(
|
|
659
|
+
operation="update",
|
|
660
|
+
record_id=record_id,
|
|
661
|
+
record_ids=[],
|
|
662
|
+
normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
|
|
663
|
+
submit_type=submit_type_value,
|
|
664
|
+
)
|
|
665
|
+
if preflight_data.get("blockers"):
|
|
666
|
+
return self._record_write_blocked_response(
|
|
667
|
+
raw_preflight,
|
|
662
668
|
operation="update",
|
|
663
669
|
normalized_payload=normalized_payload,
|
|
664
670
|
output_profile=normalized_output_profile,
|
|
665
671
|
human_review=True,
|
|
672
|
+
target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
|
|
666
673
|
)
|
|
667
674
|
raw_apply = self.record_update(
|
|
668
675
|
profile=profile,
|
|
669
676
|
app_key=app_key,
|
|
670
677
|
apply_id=record_id,
|
|
671
|
-
answers=normalized_answers,
|
|
678
|
+
answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
|
|
672
679
|
fields={},
|
|
673
680
|
role=1,
|
|
674
681
|
verify_write=verify_write,
|
|
@@ -680,6 +687,7 @@ class RecordTools(ToolBase):
|
|
|
680
687
|
normalized_payload=normalized_payload,
|
|
681
688
|
output_profile=normalized_output_profile,
|
|
682
689
|
human_review=True,
|
|
690
|
+
preflight=raw_preflight,
|
|
683
691
|
)
|
|
684
692
|
|
|
685
693
|
if values or set:
|
|
@@ -694,22 +702,6 @@ class RecordTools(ToolBase):
|
|
|
694
702
|
"answers": [],
|
|
695
703
|
"submit_type": submit_type_value,
|
|
696
704
|
}
|
|
697
|
-
if normalized_mode == "plan":
|
|
698
|
-
return {
|
|
699
|
-
"profile": profile,
|
|
700
|
-
"ok": True,
|
|
701
|
-
"request_route": None,
|
|
702
|
-
"warnings": [],
|
|
703
|
-
"output_profile": normalized_output_profile,
|
|
704
|
-
"data": {
|
|
705
|
-
"action": {"operation": "delete", "mode": "plan"},
|
|
706
|
-
"resource": {"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": delete_ids},
|
|
707
|
-
"verification": None,
|
|
708
|
-
"normalized_payload": normalized_payload,
|
|
709
|
-
"blockers": [],
|
|
710
|
-
"human_review": self._record_write_human_review_payload("delete", enabled=True),
|
|
711
|
-
},
|
|
712
|
-
}
|
|
713
705
|
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=delete_ids)
|
|
714
706
|
return self._record_write_apply_response(
|
|
715
707
|
raw_apply,
|
|
@@ -717,6 +709,7 @@ class RecordTools(ToolBase):
|
|
|
717
709
|
normalized_payload=normalized_payload,
|
|
718
710
|
output_profile=normalized_output_profile,
|
|
719
711
|
human_review=True,
|
|
712
|
+
preflight=None,
|
|
720
713
|
)
|
|
721
714
|
|
|
722
715
|
def _schema_field_payload(self, field: FormField) -> JSONObject:
|
|
@@ -1259,7 +1252,6 @@ class RecordTools(ToolBase):
|
|
|
1259
1252
|
requested_pages = int(analysis_paging["requested_pages"])
|
|
1260
1253
|
scan_max_pages = int(analysis_paging["scan_max_pages"])
|
|
1261
1254
|
auto_expand_pages = bool(analysis_paging["auto_expand_pages"])
|
|
1262
|
-
query_id = _query_id()
|
|
1263
1255
|
pages_to_scan = min(max(requested_pages, 1), max(scan_max_pages, 1))
|
|
1264
1256
|
current_page = 1
|
|
1265
1257
|
scanned_pages = 0
|
|
@@ -1349,82 +1341,101 @@ class RecordTools(ToolBase):
|
|
|
1349
1341
|
]
|
|
1350
1342
|
all_rows = self._sort_analyze_rows(all_rows, sort, dimensions, metrics)
|
|
1351
1343
|
rows_truncated = len(all_rows) > limit
|
|
1352
|
-
|
|
1353
|
-
rows = limited_rows
|
|
1354
|
-
rows_returned = len(limited_rows)
|
|
1344
|
+
rows = all_rows[:limit]
|
|
1355
1345
|
group_count = len(all_rows)
|
|
1356
1346
|
statement_scope = "returned_groups_only" if rows_truncated else "full_population"
|
|
1357
1347
|
else:
|
|
1358
1348
|
rows_truncated = False
|
|
1359
1349
|
rows = [{"dimensions": {}, "metrics": metric_totals}]
|
|
1360
|
-
rows_returned = 1
|
|
1361
1350
|
group_count = 1
|
|
1362
1351
|
statement_scope = "full_population"
|
|
1363
1352
|
raw_scan_complete = not has_more
|
|
1364
1353
|
completeness_status = "complete" if raw_scan_complete else "incomplete"
|
|
1365
1354
|
reason_code = "LOCAL_VIEW_FILTERING" if local_filtering and raw_scan_complete else ("SOURCE_EXHAUSTED" if raw_scan_complete else "SCAN_LIMIT_HIT")
|
|
1366
|
-
|
|
1367
|
-
"
|
|
1368
|
-
"
|
|
1369
|
-
|
|
1355
|
+
query_payload: JSONObject = {
|
|
1356
|
+
"app_key": app_key,
|
|
1357
|
+
"dimensions": [
|
|
1358
|
+
{
|
|
1359
|
+
"field_id": cast(FormField, item["field"]).que_id,
|
|
1360
|
+
"alias": item["alias"],
|
|
1361
|
+
"bucket": item["bucket"],
|
|
1362
|
+
}
|
|
1363
|
+
for item in dimensions
|
|
1364
|
+
],
|
|
1365
|
+
"metrics": [
|
|
1366
|
+
{
|
|
1367
|
+
"op": item["op"],
|
|
1368
|
+
**(
|
|
1369
|
+
{"field_id": cast(FormField, item["field"]).que_id}
|
|
1370
|
+
if item["field"] is not None
|
|
1371
|
+
else {}
|
|
1372
|
+
),
|
|
1373
|
+
"alias": item["alias"],
|
|
1374
|
+
}
|
|
1375
|
+
for item in metrics
|
|
1376
|
+
],
|
|
1377
|
+
"filters": [
|
|
1378
|
+
{
|
|
1379
|
+
"field_id": cast(FormField, item["field"]).que_id,
|
|
1380
|
+
"op": item["op"],
|
|
1381
|
+
**({"value": item.get("value")} if item.get("value") is not None else {}),
|
|
1382
|
+
}
|
|
1383
|
+
for item in filters
|
|
1384
|
+
],
|
|
1385
|
+
"sort": [{"by": item["by"], "order": item["order"]} for item in sort],
|
|
1386
|
+
"limit": limit,
|
|
1387
|
+
"strict_full": strict_full,
|
|
1370
1388
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
for item in filters
|
|
1400
|
-
],
|
|
1401
|
-
"applied_sort": [{"by": item["by"], "order": item["order"]} for item in sort],
|
|
1402
|
-
"view": _view_selection_payload(view_selection),
|
|
1403
|
-
},
|
|
1404
|
-
"rows": rows,
|
|
1405
|
-
"totals": totals,
|
|
1406
|
-
"completeness": {
|
|
1407
|
-
"status": completeness_status,
|
|
1408
|
-
"reason_code": reason_code,
|
|
1409
|
-
"local_filtering_applied": local_filtering,
|
|
1410
|
-
"safe_for_final_conclusion": completeness_status == "complete",
|
|
1411
|
-
},
|
|
1412
|
-
"presentation": {
|
|
1413
|
-
"row_limit": limit,
|
|
1414
|
-
"rows_returned": rows_returned,
|
|
1415
|
-
"rows_truncated": rows_truncated,
|
|
1416
|
-
"statement_scope": statement_scope,
|
|
1417
|
-
},
|
|
1418
|
-
"warnings": self._build_analyze_warnings(local_filtering=local_filtering, rows_truncated=rows_truncated),
|
|
1389
|
+
view_payload = _view_selection_payload(view_selection)
|
|
1390
|
+
if view_payload:
|
|
1391
|
+
query_payload["view"] = view_payload
|
|
1392
|
+
|
|
1393
|
+
ranking: JSONObject | None = None
|
|
1394
|
+
if dimensions and sort:
|
|
1395
|
+
primary_sort = sort[0]
|
|
1396
|
+
ranking = {
|
|
1397
|
+
"order_by": primary_sort["by"],
|
|
1398
|
+
"order_direction": primary_sort["order"],
|
|
1399
|
+
"ranked": True,
|
|
1400
|
+
}
|
|
1401
|
+
rows = [
|
|
1402
|
+
{
|
|
1403
|
+
"dimensions": cast(JSONObject, row["dimensions"]),
|
|
1404
|
+
"metrics": cast(JSONObject, row["metrics"]),
|
|
1405
|
+
"rank": idx,
|
|
1406
|
+
}
|
|
1407
|
+
for idx, row in enumerate(rows, start=1)
|
|
1408
|
+
]
|
|
1409
|
+
|
|
1410
|
+
warnings = self._build_analyze_warnings(local_filtering=local_filtering, rows_truncated=rows_truncated)
|
|
1411
|
+
completeness: JSONObject = {
|
|
1412
|
+
"status": completeness_status,
|
|
1413
|
+
"safe_for_final_conclusion": completeness_status == "complete",
|
|
1414
|
+
"rows_truncated": rows_truncated,
|
|
1415
|
+
"statement_scope": statement_scope,
|
|
1416
|
+
"warnings": warnings,
|
|
1419
1417
|
}
|
|
1418
|
+
if completeness_status != "complete":
|
|
1419
|
+
completeness["reason_code"] = reason_code
|
|
1420
|
+
|
|
1420
1421
|
response: JSONObject = {
|
|
1421
|
-
"profile": profile,
|
|
1422
|
-
"ws_id": session_profile.selected_ws_id,
|
|
1423
1422
|
"ok": True,
|
|
1424
1423
|
"status": "success" if raw_scan_complete else "partial",
|
|
1425
|
-
"
|
|
1426
|
-
"
|
|
1427
|
-
|
|
1424
|
+
"query": query_payload,
|
|
1425
|
+
"result": {
|
|
1426
|
+
"totals": {
|
|
1427
|
+
"metric_totals": metric_totals,
|
|
1428
|
+
},
|
|
1429
|
+
"rows": rows,
|
|
1430
|
+
"row_count": len(rows),
|
|
1431
|
+
},
|
|
1432
|
+
"ranking": ranking,
|
|
1433
|
+
"ratios": None,
|
|
1434
|
+
"completeness": completeness,
|
|
1435
|
+
"presentation": {
|
|
1436
|
+
"returned_group_count": len(rows),
|
|
1437
|
+
"total_group_count": group_count,
|
|
1438
|
+
},
|
|
1428
1439
|
}
|
|
1429
1440
|
if strict_full and not raw_scan_complete:
|
|
1430
1441
|
response["ok"] = False
|
|
@@ -1435,13 +1446,15 @@ class RecordTools(ToolBase):
|
|
|
1435
1446
|
"fix_hint": "Narrow the scope with view/filter constraints, or retry after reducing the dataset size.",
|
|
1436
1447
|
}
|
|
1437
1448
|
if output_profile == "verbose":
|
|
1438
|
-
response["
|
|
1449
|
+
response["debug"] = {
|
|
1450
|
+
"request_route": self._request_route_payload(context),
|
|
1439
1451
|
"elapsed_ms": int((time.perf_counter() - started_at) * 1000),
|
|
1440
1452
|
"backend_total_hint": scan_control.get("backend_total_count", result_amount),
|
|
1441
1453
|
"backend_page_amount": scan_control.get("backend_page_amount"),
|
|
1442
1454
|
"source_pages": source_pages,
|
|
1443
|
-
"raw_scan_complete": raw_scan_complete,
|
|
1444
1455
|
"scan_control": scan_control,
|
|
1456
|
+
"reason_code": reason_code,
|
|
1457
|
+
"local_filtering_applied": local_filtering,
|
|
1445
1458
|
}
|
|
1446
1459
|
return response
|
|
1447
1460
|
|
|
@@ -1559,19 +1572,9 @@ class RecordTools(ToolBase):
|
|
|
1559
1572
|
def _build_analyze_warnings(self, *, local_filtering: bool, rows_truncated: bool) -> list[JSONObject]:
|
|
1560
1573
|
warnings: list[JSONObject] = []
|
|
1561
1574
|
if local_filtering:
|
|
1562
|
-
warnings.append(
|
|
1563
|
-
{
|
|
1564
|
-
"code": "LOCAL_FILTERING_APPLIED",
|
|
1565
|
-
"message": "Current analysis applies local filtering after scanning source pages.",
|
|
1566
|
-
}
|
|
1567
|
-
)
|
|
1575
|
+
warnings.append({"code": "LOCAL_VIEW_FILTERING"})
|
|
1568
1576
|
if rows_truncated:
|
|
1569
|
-
warnings.append(
|
|
1570
|
-
{
|
|
1571
|
-
"code": "ROWS_TRUNCATED",
|
|
1572
|
-
"message": "Result rows were truncated by limit; totals remain based on the full analyzed result set.",
|
|
1573
|
-
}
|
|
1574
|
-
)
|
|
1577
|
+
warnings.append({"code": "ROWS_TRUNCATED"})
|
|
1575
1578
|
return warnings
|
|
1576
1579
|
|
|
1577
1580
|
def record_write_plan(
|
|
@@ -1589,123 +1592,180 @@ class RecordTools(ToolBase):
|
|
|
1589
1592
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
1590
1593
|
inferred_operation = operation if operation in {"create", "update"} else ("update" if apply_id else "create")
|
|
1591
1594
|
|
|
1595
|
+
return self._preflight_record_write(
|
|
1596
|
+
profile=profile,
|
|
1597
|
+
operation=inferred_operation,
|
|
1598
|
+
app_key=app_key,
|
|
1599
|
+
apply_id=apply_id,
|
|
1600
|
+
answers=answers or [],
|
|
1601
|
+
fields=fields or {},
|
|
1602
|
+
force_refresh_form=force_refresh_form,
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1605
|
+
def _preflight_record_write(
|
|
1606
|
+
self,
|
|
1607
|
+
*,
|
|
1608
|
+
profile: str,
|
|
1609
|
+
operation: str,
|
|
1610
|
+
app_key: str,
|
|
1611
|
+
apply_id: int | None,
|
|
1612
|
+
answers: list[JSONObject],
|
|
1613
|
+
fields: JSONObject,
|
|
1614
|
+
force_refresh_form: bool,
|
|
1615
|
+
) -> JSONObject:
|
|
1616
|
+
if not app_key:
|
|
1617
|
+
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
1618
|
+
|
|
1592
1619
|
def runner(session_profile, context):
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
profile,
|
|
1604
|
-
context,
|
|
1605
|
-
app_key,
|
|
1606
|
-
answers=normalized_answers_input,
|
|
1607
|
-
fields=normalized_fields,
|
|
1608
|
-
force_refresh_form=force_refresh_form,
|
|
1609
|
-
)
|
|
1610
|
-
except RecordInputError as error:
|
|
1611
|
-
invalid_fields.append(
|
|
1612
|
-
{
|
|
1613
|
-
"location": _stringify_json(error.details.get("location") if error.details else None),
|
|
1614
|
-
"message": error.message,
|
|
1615
|
-
"error_code": error.error_code,
|
|
1616
|
-
"field": error.details.get("field") if error.details and isinstance(error.details.get("field"), dict) else None,
|
|
1617
|
-
"expected_format": error.details.get("expected_format") if error.details and isinstance(error.details.get("expected_format"), dict) else None,
|
|
1618
|
-
"received_value": error.details.get("received_value") if error.details else None,
|
|
1619
|
-
}
|
|
1620
|
-
)
|
|
1621
|
-
readonly_or_system_fields = [
|
|
1622
|
-
{
|
|
1623
|
-
"que_id": entry.get("que_id"),
|
|
1624
|
-
"que_title": entry.get("que_title"),
|
|
1625
|
-
"que_type": entry.get("que_type"),
|
|
1626
|
-
"readonly": entry.get("readonly"),
|
|
1627
|
-
"system": entry.get("system"),
|
|
1628
|
-
"source": entry.get("source"),
|
|
1629
|
-
"requested": entry.get("requested"),
|
|
1630
|
-
}
|
|
1631
|
-
for entry in resolved_fields
|
|
1632
|
-
if bool(entry.get("resolved")) and (bool(entry.get("readonly")) or bool(entry.get("system")))
|
|
1633
|
-
]
|
|
1634
|
-
provided_field_ids = {
|
|
1635
|
-
str(answer.get("queId"))
|
|
1636
|
-
for answer in normalized_answers
|
|
1637
|
-
if isinstance(answer.get("queId"), int) and int(answer["queId"]) > 0
|
|
1638
|
-
}
|
|
1639
|
-
missing_required_fields = []
|
|
1640
|
-
for field in index.by_id.values():
|
|
1641
|
-
if not field.required or str(field.que_id) in provided_field_ids:
|
|
1642
|
-
continue
|
|
1643
|
-
missing_required_fields.append(
|
|
1644
|
-
{
|
|
1645
|
-
"que_id": field.que_id,
|
|
1646
|
-
"que_title": field.que_title,
|
|
1647
|
-
"que_type": field.que_type,
|
|
1648
|
-
"reason": "required field not provided",
|
|
1649
|
-
}
|
|
1650
|
-
)
|
|
1651
|
-
question_relations = _collect_question_relations(schema)
|
|
1652
|
-
option_links = _collect_option_links(resolved_fields)
|
|
1653
|
-
validation = {
|
|
1654
|
-
"valid": not invalid_fields and not missing_required_fields and not readonly_or_system_fields,
|
|
1655
|
-
"missing_required_fields": missing_required_fields,
|
|
1656
|
-
"likely_hidden_required_fields": [],
|
|
1657
|
-
"readonly_or_system_fields": readonly_or_system_fields,
|
|
1658
|
-
"invalid_fields": invalid_fields,
|
|
1659
|
-
"warnings": [
|
|
1660
|
-
"record_write_plan is a static preflight built from form metadata; runtime visibility and dynamic linkage can still reject writes."
|
|
1661
|
-
],
|
|
1662
|
-
}
|
|
1663
|
-
blockers = []
|
|
1664
|
-
if invalid_fields:
|
|
1665
|
-
blockers.append("payload contains invalid field values")
|
|
1666
|
-
if missing_required_fields:
|
|
1667
|
-
blockers.append("required fields are missing")
|
|
1668
|
-
if readonly_or_system_fields:
|
|
1669
|
-
blockers.append("payload writes readonly or system-managed fields")
|
|
1670
|
-
if question_relations:
|
|
1671
|
-
validation["warnings"].append("form contains questionRelations; linked visibility and runtime required rules may differ at submit time.")
|
|
1672
|
-
actions = ["Use record_schema_get when field titles or field ids are ambiguous."]
|
|
1673
|
-
if support_matrix["restricted"]:
|
|
1674
|
-
actions.append("Review write_format.required_presteps for restricted fields before submit.")
|
|
1675
|
-
if invalid_fields:
|
|
1676
|
-
actions.append("Fix invalid_fields before calling record_create or record_update.")
|
|
1677
|
-
if missing_required_fields:
|
|
1678
|
-
actions.append("Fill missing required fields before submit.")
|
|
1679
|
-
if readonly_or_system_fields:
|
|
1680
|
-
actions.append("Remove readonly/system fields from payload before submit.")
|
|
1681
|
-
if question_relations:
|
|
1682
|
-
actions.append("Treat ready_to_submit as static-only because linked fields can still appear at runtime.")
|
|
1620
|
+
plan_data = self._build_record_write_preflight(
|
|
1621
|
+
profile=profile,
|
|
1622
|
+
context=context,
|
|
1623
|
+
operation=operation,
|
|
1624
|
+
app_key=app_key,
|
|
1625
|
+
apply_id=apply_id,
|
|
1626
|
+
answers=answers,
|
|
1627
|
+
fields=fields,
|
|
1628
|
+
force_refresh_form=force_refresh_form,
|
|
1629
|
+
)
|
|
1683
1630
|
return {
|
|
1684
1631
|
"profile": profile,
|
|
1685
1632
|
"ws_id": session_profile.selected_ws_id,
|
|
1686
1633
|
"ok": True,
|
|
1687
1634
|
"request_route": self._request_route_payload(context),
|
|
1688
|
-
"data":
|
|
1689
|
-
"operation": inferred_operation,
|
|
1690
|
-
"app_key": app_key,
|
|
1691
|
-
"apply_id": apply_id,
|
|
1692
|
-
"normalized_answers": normalized_answers,
|
|
1693
|
-
"resolved_fields": resolved_fields,
|
|
1694
|
-
"support_matrix": support_matrix,
|
|
1695
|
-
"validation": validation,
|
|
1696
|
-
"dependencies": {
|
|
1697
|
-
"question_relations_present": bool(question_relations),
|
|
1698
|
-
"question_relations": question_relations,
|
|
1699
|
-
"option_links": option_links,
|
|
1700
|
-
},
|
|
1701
|
-
"ready_to_submit": validation["valid"],
|
|
1702
|
-
"blockers": blockers,
|
|
1703
|
-
"recommended_next_actions": actions,
|
|
1704
|
-
},
|
|
1635
|
+
"data": plan_data,
|
|
1705
1636
|
}
|
|
1706
1637
|
|
|
1707
1638
|
return self._run_record_tool(profile, runner)
|
|
1708
1639
|
|
|
1640
|
+
def _build_record_write_preflight(
|
|
1641
|
+
self,
|
|
1642
|
+
*,
|
|
1643
|
+
profile: str,
|
|
1644
|
+
context, # type: ignore[no-untyped-def]
|
|
1645
|
+
operation: str,
|
|
1646
|
+
app_key: str,
|
|
1647
|
+
apply_id: int | None,
|
|
1648
|
+
answers: list[JSONObject],
|
|
1649
|
+
fields: JSONObject,
|
|
1650
|
+
force_refresh_form: bool,
|
|
1651
|
+
) -> JSONObject:
|
|
1652
|
+
schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
|
|
1653
|
+
index = _build_field_index(schema)
|
|
1654
|
+
normalized_fields = fields or {}
|
|
1655
|
+
normalized_answers_input = answers or []
|
|
1656
|
+
resolved_fields = self._collect_write_plan_field_refs(fields=normalized_fields, answers=normalized_answers_input, index=index)
|
|
1657
|
+
support_matrix = _summarize_write_support(resolved_fields)
|
|
1658
|
+
invalid_fields: list[JSONObject] = []
|
|
1659
|
+
normalized_answers: list[JSONObject] = []
|
|
1660
|
+
try:
|
|
1661
|
+
normalized_answers = self._resolve_answers(
|
|
1662
|
+
profile,
|
|
1663
|
+
context,
|
|
1664
|
+
app_key,
|
|
1665
|
+
answers=normalized_answers_input,
|
|
1666
|
+
fields=normalized_fields,
|
|
1667
|
+
force_refresh_form=force_refresh_form,
|
|
1668
|
+
)
|
|
1669
|
+
except RecordInputError as error:
|
|
1670
|
+
invalid_fields.append(
|
|
1671
|
+
{
|
|
1672
|
+
"location": _stringify_json(error.details.get("location") if error.details else None),
|
|
1673
|
+
"message": error.message,
|
|
1674
|
+
"error_code": error.error_code,
|
|
1675
|
+
"field": error.details.get("field") if error.details and isinstance(error.details.get("field"), dict) else None,
|
|
1676
|
+
"expected_format": error.details.get("expected_format") if error.details and isinstance(error.details.get("expected_format"), dict) else None,
|
|
1677
|
+
"received_value": error.details.get("received_value") if error.details else None,
|
|
1678
|
+
}
|
|
1679
|
+
)
|
|
1680
|
+
readonly_or_system_fields = [
|
|
1681
|
+
{
|
|
1682
|
+
"que_id": entry.get("que_id"),
|
|
1683
|
+
"que_title": entry.get("que_title"),
|
|
1684
|
+
"que_type": entry.get("que_type"),
|
|
1685
|
+
"readonly": entry.get("readonly"),
|
|
1686
|
+
"system": entry.get("system"),
|
|
1687
|
+
"source": entry.get("source"),
|
|
1688
|
+
"requested": entry.get("requested"),
|
|
1689
|
+
}
|
|
1690
|
+
for entry in resolved_fields
|
|
1691
|
+
if bool(entry.get("resolved")) and (bool(entry.get("readonly")) or bool(entry.get("system")))
|
|
1692
|
+
]
|
|
1693
|
+
provided_field_ids = {
|
|
1694
|
+
str(answer.get("queId"))
|
|
1695
|
+
for answer in normalized_answers
|
|
1696
|
+
if isinstance(answer.get("queId"), int) and int(answer["queId"]) > 0
|
|
1697
|
+
}
|
|
1698
|
+
missing_required_fields = []
|
|
1699
|
+
for field in index.by_id.values():
|
|
1700
|
+
if not field.required or str(field.que_id) in provided_field_ids:
|
|
1701
|
+
continue
|
|
1702
|
+
missing_required_fields.append(
|
|
1703
|
+
{
|
|
1704
|
+
"que_id": field.que_id,
|
|
1705
|
+
"que_title": field.que_title,
|
|
1706
|
+
"que_type": field.que_type,
|
|
1707
|
+
"reason": "required field not provided",
|
|
1708
|
+
}
|
|
1709
|
+
)
|
|
1710
|
+
question_relations = _collect_question_relations(schema)
|
|
1711
|
+
option_links = _collect_option_links(resolved_fields)
|
|
1712
|
+
validation_warnings = [
|
|
1713
|
+
"record_write performs static preflight from form metadata before apply; runtime visibility and dynamic linkage can still reject writes."
|
|
1714
|
+
]
|
|
1715
|
+
if question_relations:
|
|
1716
|
+
validation_warnings.append(
|
|
1717
|
+
"form contains questionRelations; linked visibility and runtime required rules may differ at submit time."
|
|
1718
|
+
)
|
|
1719
|
+
validation = {
|
|
1720
|
+
"valid": not invalid_fields and not missing_required_fields and not readonly_or_system_fields,
|
|
1721
|
+
"missing_required_fields": missing_required_fields,
|
|
1722
|
+
"likely_hidden_required_fields": [],
|
|
1723
|
+
"readonly_or_system_fields": readonly_or_system_fields,
|
|
1724
|
+
"invalid_fields": invalid_fields,
|
|
1725
|
+
"warnings": validation_warnings,
|
|
1726
|
+
}
|
|
1727
|
+
field_errors = self._record_write_field_errors(
|
|
1728
|
+
invalid_fields=invalid_fields,
|
|
1729
|
+
missing_required_fields=missing_required_fields,
|
|
1730
|
+
readonly_or_system_fields=readonly_or_system_fields,
|
|
1731
|
+
)
|
|
1732
|
+
blockers = []
|
|
1733
|
+
if invalid_fields:
|
|
1734
|
+
blockers.append("payload contains invalid field values")
|
|
1735
|
+
if missing_required_fields:
|
|
1736
|
+
blockers.append("required fields are missing")
|
|
1737
|
+
if readonly_or_system_fields:
|
|
1738
|
+
blockers.append("payload writes readonly or system-managed fields")
|
|
1739
|
+
actions = ["Use record_schema_get when field titles or field ids are ambiguous."]
|
|
1740
|
+
if support_matrix["restricted"]:
|
|
1741
|
+
actions.append("Review write_format.required_presteps for restricted fields before submit.")
|
|
1742
|
+
if invalid_fields:
|
|
1743
|
+
actions.append("Fix invalid_fields before applying the write.")
|
|
1744
|
+
if missing_required_fields:
|
|
1745
|
+
actions.append("Fill missing required fields before applying the write.")
|
|
1746
|
+
if readonly_or_system_fields:
|
|
1747
|
+
actions.append("Remove readonly/system fields from payload before applying the write.")
|
|
1748
|
+
if question_relations:
|
|
1749
|
+
actions.append("Treat static preflight as conservative only because linked fields can still appear at runtime.")
|
|
1750
|
+
return {
|
|
1751
|
+
"operation": operation if operation in {"create", "update"} else ("update" if apply_id else "create"),
|
|
1752
|
+
"app_key": app_key,
|
|
1753
|
+
"apply_id": apply_id,
|
|
1754
|
+
"normalized_answers": normalized_answers,
|
|
1755
|
+
"resolved_fields": resolved_fields,
|
|
1756
|
+
"support_matrix": support_matrix,
|
|
1757
|
+
"validation": validation,
|
|
1758
|
+
"field_errors": field_errors,
|
|
1759
|
+
"dependencies": {
|
|
1760
|
+
"question_relations_present": bool(question_relations),
|
|
1761
|
+
"question_relations": question_relations,
|
|
1762
|
+
"option_links": option_links,
|
|
1763
|
+
},
|
|
1764
|
+
"ready_to_submit": validation["valid"],
|
|
1765
|
+
"blockers": blockers,
|
|
1766
|
+
"recommended_next_actions": actions,
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1709
1769
|
def record_query(
|
|
1710
1770
|
self,
|
|
1711
1771
|
*,
|
|
@@ -2950,6 +3010,23 @@ class RecordTools(ToolBase):
|
|
|
2950
3010
|
normalized.append(payload)
|
|
2951
3011
|
return normalized
|
|
2952
3012
|
|
|
3013
|
+
def _record_write_normalized_payload(
|
|
3014
|
+
self,
|
|
3015
|
+
*,
|
|
3016
|
+
operation: str,
|
|
3017
|
+
record_id: int | None,
|
|
3018
|
+
record_ids: list[int],
|
|
3019
|
+
normalized_answers: list[JSONObject],
|
|
3020
|
+
submit_type: int,
|
|
3021
|
+
) -> JSONObject:
|
|
3022
|
+
return {
|
|
3023
|
+
"operation": operation,
|
|
3024
|
+
"record_id": record_id,
|
|
3025
|
+
"record_ids": record_ids,
|
|
3026
|
+
"answers": normalized_answers,
|
|
3027
|
+
"submit_type": submit_type,
|
|
3028
|
+
}
|
|
3029
|
+
|
|
2953
3030
|
def _record_write_human_review_payload(self, operation: str, *, enabled: bool) -> JSONObject | None:
|
|
2954
3031
|
if not enabled:
|
|
2955
3032
|
return None
|
|
@@ -2959,38 +3036,41 @@ class RecordTools(ToolBase):
|
|
|
2959
3036
|
"message": "Read the current record first and confirm the exact target before applying this high-risk write.",
|
|
2960
3037
|
}
|
|
2961
3038
|
|
|
2962
|
-
def
|
|
3039
|
+
def _record_write_blocked_response(
|
|
2963
3040
|
self,
|
|
2964
|
-
|
|
3041
|
+
raw_preflight: JSONObject,
|
|
2965
3042
|
*,
|
|
2966
3043
|
operation: str,
|
|
2967
3044
|
normalized_payload: JSONObject,
|
|
2968
3045
|
output_profile: str,
|
|
2969
3046
|
human_review: bool,
|
|
3047
|
+
target_resource: JSONObject,
|
|
2970
3048
|
) -> JSONObject:
|
|
2971
|
-
plan_data = cast(JSONObject,
|
|
3049
|
+
plan_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
2972
3050
|
validation = cast(JSONObject, plan_data.get("validation", {}))
|
|
3051
|
+
field_errors = self._record_write_public_field_errors(plan_data)
|
|
2973
3052
|
warnings_payload = validation.get("warnings", [])
|
|
2974
|
-
warnings = [{"code": "
|
|
3053
|
+
warnings = [{"code": "PREFLIGHT_WARNING", "message": str(item)} for item in warnings_payload] if isinstance(warnings_payload, list) else []
|
|
2975
3054
|
response: JSONObject = {
|
|
2976
|
-
"profile":
|
|
2977
|
-
"ws_id":
|
|
2978
|
-
"ok":
|
|
2979
|
-
"request_route":
|
|
3055
|
+
"profile": raw_preflight.get("profile"),
|
|
3056
|
+
"ws_id": raw_preflight.get("ws_id"),
|
|
3057
|
+
"ok": False,
|
|
3058
|
+
"request_route": raw_preflight.get("request_route"),
|
|
2980
3059
|
"warnings": warnings,
|
|
2981
3060
|
"output_profile": output_profile,
|
|
2982
3061
|
"data": {
|
|
2983
|
-
"action": {"operation": operation, "
|
|
2984
|
-
"resource":
|
|
3062
|
+
"action": {"operation": operation, "executed": False},
|
|
3063
|
+
"resource": target_resource,
|
|
2985
3064
|
"verification": None,
|
|
2986
3065
|
"normalized_payload": normalized_payload,
|
|
2987
3066
|
"blockers": plan_data.get("blockers", []),
|
|
3067
|
+
"field_errors": field_errors,
|
|
2988
3068
|
"human_review": self._record_write_human_review_payload(operation, enabled=human_review),
|
|
2989
3069
|
},
|
|
2990
3070
|
}
|
|
2991
3071
|
if output_profile == "verbose":
|
|
2992
3072
|
response["data"]["debug"] = {
|
|
2993
|
-
"
|
|
3073
|
+
"preflight": plan_data,
|
|
2994
3074
|
}
|
|
2995
3075
|
return response
|
|
2996
3076
|
|
|
@@ -3002,31 +3082,107 @@ class RecordTools(ToolBase):
|
|
|
3002
3082
|
normalized_payload: JSONObject,
|
|
3003
3083
|
output_profile: str,
|
|
3004
3084
|
human_review: bool,
|
|
3085
|
+
preflight: JSONObject | None,
|
|
3005
3086
|
) -> JSONObject:
|
|
3087
|
+
preflight_data = cast(JSONObject, preflight.get("data", {})) if isinstance(preflight, dict) else {}
|
|
3088
|
+
validation = cast(JSONObject, preflight_data.get("validation", {}))
|
|
3089
|
+
warnings_payload = validation.get("warnings", [])
|
|
3090
|
+
warnings = [{"code": "PREFLIGHT_WARNING", "message": str(item)} for item in warnings_payload] if isinstance(warnings_payload, list) else []
|
|
3006
3091
|
response: JSONObject = {
|
|
3007
3092
|
"profile": raw_apply.get("profile"),
|
|
3008
3093
|
"ws_id": raw_apply.get("ws_id"),
|
|
3009
3094
|
"ok": bool(raw_apply.get("ok", True)),
|
|
3010
3095
|
"request_route": raw_apply.get("request_route"),
|
|
3011
|
-
"warnings":
|
|
3096
|
+
"warnings": warnings,
|
|
3012
3097
|
"output_profile": output_profile,
|
|
3013
3098
|
"data": {
|
|
3014
|
-
"action": {"operation": operation, "
|
|
3099
|
+
"action": {"operation": operation, "executed": True},
|
|
3015
3100
|
"resource": raw_apply.get("resource"),
|
|
3016
3101
|
"verification": raw_apply.get("verification"),
|
|
3017
3102
|
"normalized_payload": normalized_payload,
|
|
3018
3103
|
"blockers": [],
|
|
3104
|
+
"field_errors": [],
|
|
3019
3105
|
"human_review": self._record_write_human_review_payload(operation, enabled=human_review),
|
|
3020
3106
|
},
|
|
3021
3107
|
}
|
|
3022
3108
|
if output_profile == "verbose":
|
|
3023
|
-
|
|
3109
|
+
debug: JSONObject = {
|
|
3024
3110
|
"legacy_result": raw_apply.get("result"),
|
|
3025
3111
|
"status": raw_apply.get("status"),
|
|
3026
3112
|
"write_verified": raw_apply.get("write_verified"),
|
|
3027
3113
|
}
|
|
3114
|
+
if preflight_data:
|
|
3115
|
+
debug["preflight"] = preflight_data
|
|
3116
|
+
response["data"]["debug"] = debug
|
|
3028
3117
|
return response
|
|
3029
3118
|
|
|
3119
|
+
def _record_write_public_field_errors(self, plan_data: JSONObject) -> list[JSONObject]:
|
|
3120
|
+
existing = plan_data.get("field_errors")
|
|
3121
|
+
if isinstance(existing, list):
|
|
3122
|
+
return [cast(JSONObject, item) for item in existing if isinstance(item, dict)]
|
|
3123
|
+
validation = cast(JSONObject, plan_data.get("validation", {}))
|
|
3124
|
+
invalid_fields = cast(list[JSONObject], validation.get("invalid_fields", []))
|
|
3125
|
+
missing_required_fields = cast(list[JSONObject], validation.get("missing_required_fields", []))
|
|
3126
|
+
readonly_or_system_fields = cast(list[JSONObject], validation.get("readonly_or_system_fields", []))
|
|
3127
|
+
return self._record_write_field_errors(
|
|
3128
|
+
invalid_fields=invalid_fields,
|
|
3129
|
+
missing_required_fields=missing_required_fields,
|
|
3130
|
+
readonly_or_system_fields=readonly_or_system_fields,
|
|
3131
|
+
)
|
|
3132
|
+
|
|
3133
|
+
def _record_write_field_errors(
|
|
3134
|
+
self,
|
|
3135
|
+
*,
|
|
3136
|
+
invalid_fields: list[JSONObject],
|
|
3137
|
+
missing_required_fields: list[JSONObject],
|
|
3138
|
+
readonly_or_system_fields: list[JSONObject],
|
|
3139
|
+
) -> list[JSONObject]:
|
|
3140
|
+
errors: list[JSONObject] = []
|
|
3141
|
+
for item in invalid_fields:
|
|
3142
|
+
errors.append(
|
|
3143
|
+
{
|
|
3144
|
+
"error_code": item.get("error_code") or "INVALID_FIELD_VALUE",
|
|
3145
|
+
"message": item.get("message") or "invalid field value",
|
|
3146
|
+
"field": item.get("field"),
|
|
3147
|
+
"location": item.get("location"),
|
|
3148
|
+
"expected_format": item.get("expected_format"),
|
|
3149
|
+
"received_value": item.get("received_value"),
|
|
3150
|
+
}
|
|
3151
|
+
)
|
|
3152
|
+
for item in missing_required_fields:
|
|
3153
|
+
errors.append(
|
|
3154
|
+
{
|
|
3155
|
+
"error_code": "MISSING_REQUIRED_FIELD",
|
|
3156
|
+
"message": item.get("reason") or "required field not provided",
|
|
3157
|
+
"field": {
|
|
3158
|
+
"que_id": item.get("que_id"),
|
|
3159
|
+
"que_title": item.get("que_title"),
|
|
3160
|
+
"que_type": item.get("que_type"),
|
|
3161
|
+
},
|
|
3162
|
+
"location": "fields",
|
|
3163
|
+
"expected_format": None,
|
|
3164
|
+
"received_value": None,
|
|
3165
|
+
}
|
|
3166
|
+
)
|
|
3167
|
+
for item in readonly_or_system_fields:
|
|
3168
|
+
errors.append(
|
|
3169
|
+
{
|
|
3170
|
+
"error_code": "READONLY_OR_SYSTEM_FIELD",
|
|
3171
|
+
"message": "field is readonly or system-managed",
|
|
3172
|
+
"field": {
|
|
3173
|
+
"que_id": item.get("que_id"),
|
|
3174
|
+
"que_title": item.get("que_title"),
|
|
3175
|
+
"que_type": item.get("que_type"),
|
|
3176
|
+
"readonly": item.get("readonly"),
|
|
3177
|
+
"system": item.get("system"),
|
|
3178
|
+
},
|
|
3179
|
+
"location": item.get("source") or "fields",
|
|
3180
|
+
"expected_format": None,
|
|
3181
|
+
"received_value": item.get("requested"),
|
|
3182
|
+
}
|
|
3183
|
+
)
|
|
3184
|
+
return errors
|
|
3185
|
+
|
|
3030
3186
|
def _record_delete_many(self, *, profile: str, app_key: str, record_ids: list[int]) -> JSONObject:
|
|
3031
3187
|
if not app_key:
|
|
3032
3188
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
@@ -3122,11 +3278,11 @@ class RecordTools(ToolBase):
|
|
|
3122
3278
|
)
|
|
3123
3279
|
|
|
3124
3280
|
def _resolve_field_from_answer_item(self, item: JSONObject, index: FieldIndex) -> FormField:
|
|
3125
|
-
for key in ("queId", "que_id", "queTitle", "que_title"):
|
|
3281
|
+
for key in ("queId", "que_id", "queTitle", "que_title", "field_id", "fieldId"):
|
|
3126
3282
|
if key in item:
|
|
3127
3283
|
return self._resolve_field_selector(cast(str | int, item[key]), index, location="answers")
|
|
3128
3284
|
raise RecordInputError(
|
|
3129
|
-
message="answer item requires queId/que_id
|
|
3285
|
+
message="answer item requires queId/que_id, queTitle/que_title, or field_id",
|
|
3130
3286
|
error_code="MISSING_FIELD_SELECTOR",
|
|
3131
3287
|
fix_hint="Provide a field selector in each answer item.",
|
|
3132
3288
|
)
|
|
@@ -3452,7 +3608,10 @@ class RecordTools(ToolBase):
|
|
|
3452
3608
|
for item in answers:
|
|
3453
3609
|
if not isinstance(item, dict):
|
|
3454
3610
|
continue
|
|
3455
|
-
selector = item.get(
|
|
3611
|
+
selector = item.get(
|
|
3612
|
+
"field_id",
|
|
3613
|
+
item.get("fieldId", item.get("queId", item.get("que_id", item.get("queTitle", item.get("que_title"))))),
|
|
3614
|
+
)
|
|
3456
3615
|
if selector is None:
|
|
3457
3616
|
continue
|
|
3458
3617
|
refs.append(self._resolve_write_plan_field_ref("answers", str(selector), index))
|