@qingflow-tech/qingflow-app-user-mcp 1.0.2 → 1.0.4
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 +2 -2
- package/docs/local-agent-install.md +9 -3
- package/npm/lib/runtime.mjs +10 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-user/SKILL.md +21 -12
- package/skills/qingflow-app-user/references/data-gotchas.md +1 -1
- package/skills/qingflow-app-user/references/public-surface-sync.md +70 -0
- package/skills/qingflow-app-user/references/record-patterns.md +1 -1
- package/skills/qingflow-record-analysis/SKILL.md +44 -2
- package/skills/qingflow-record-insert/SKILL.md +3 -0
- package/skills/qingflow-record-update/SKILL.md +3 -0
- package/skills/qingflow-task-ops/SKILL.md +31 -10
- package/src/qingflow_mcp/__init__.py +33 -1
- package/src/qingflow_mcp/backend_client.py +109 -0
- package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
- package/src/qingflow_mcp/builder_facade/models.py +58 -9
- package/src/qingflow_mcp/builder_facade/service.py +1711 -240
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/app.py +47 -1
- package/src/qingflow_mcp/cli/commands/auth.py +63 -0
- package/src/qingflow_mcp/cli/commands/builder.py +11 -3
- package/src/qingflow_mcp/cli/commands/exports.py +111 -0
- package/src/qingflow_mcp/cli/commands/record.py +5 -5
- package/src/qingflow_mcp/cli/commands/task.py +701 -27
- package/src/qingflow_mcp/cli/commands/workspace.py +84 -0
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +424 -50
- package/src/qingflow_mcp/cli/interaction.py +72 -0
- package/src/qingflow_mcp/cli/main.py +11 -1
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/cli/terminal_ui.py +218 -0
- package/src/qingflow_mcp/config.py +1 -1
- package/src/qingflow_mcp/errors.py +4 -4
- package/src/qingflow_mcp/export_store.py +14 -0
- package/src/qingflow_mcp/id_utils.py +49 -0
- package/src/qingflow_mcp/public_surface.py +16 -1
- package/src/qingflow_mcp/response_trim.py +394 -9
- package/src/qingflow_mcp/server.py +26 -0
- package/src/qingflow_mcp/server_app_builder.py +15 -1
- package/src/qingflow_mcp/server_app_user.py +113 -0
- package/src/qingflow_mcp/session_store.py +126 -21
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
- package/src/qingflow_mcp/solution/executor.py +2 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +107 -34
- package/src/qingflow_mcp/tools/app_tools.py +1 -0
- package/src/qingflow_mcp/tools/auth_tools.py +243 -9
- package/src/qingflow_mcp/tools/base.py +6 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
- package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
- package/src/qingflow_mcp/tools/export_tools.py +1565 -0
- package/src/qingflow_mcp/tools/import_tools.py +78 -4
- package/src/qingflow_mcp/tools/record_tools.py +551 -165
- package/src/qingflow_mcp/tools/resource_read_tools.py +154 -33
- package/src/qingflow_mcp/tools/task_context_tools.py +917 -141
- package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
|
@@ -13,6 +13,7 @@ from mcp.server.fastmcp import FastMCP
|
|
|
13
13
|
|
|
14
14
|
from ..config import DEFAULT_PROFILE, DEFAULT_RECORD_LIST_TYPE
|
|
15
15
|
from ..errors import QingflowApiError, raise_tool_error
|
|
16
|
+
from ..id_utils import normalize_positive_id_int, stringify_backend_id
|
|
16
17
|
from ..json_types import JSONObject, JSONScalar, JSONValue
|
|
17
18
|
from ..list_type_labels import (
|
|
18
19
|
SYSTEM_VIEW_DEFINITIONS,
|
|
@@ -180,6 +181,16 @@ class AccessibleViewRoute:
|
|
|
180
181
|
view_type: str | None = None
|
|
181
182
|
|
|
182
183
|
|
|
184
|
+
@dataclass(slots=True)
|
|
185
|
+
class RecordContextRouteProbe:
|
|
186
|
+
route: AccessibleViewRoute
|
|
187
|
+
answer_list: list[JSONObject] | None
|
|
188
|
+
used_list_type: int | None
|
|
189
|
+
readable: bool
|
|
190
|
+
transport_error: bool
|
|
191
|
+
error_payload: JSONObject | None
|
|
192
|
+
|
|
193
|
+
|
|
183
194
|
@dataclass(slots=True)
|
|
184
195
|
class WorkflowNodeRef:
|
|
185
196
|
workflow_node_id: int
|
|
@@ -274,7 +285,7 @@ class RecordTools(ToolBase):
|
|
|
274
285
|
profile: str = DEFAULT_PROFILE,
|
|
275
286
|
app_key: str = "",
|
|
276
287
|
field_id: int = 0,
|
|
277
|
-
record_id:
|
|
288
|
+
record_id: str | None = None,
|
|
278
289
|
workflow_node_id: int | None = None,
|
|
279
290
|
fields: JSONObject | None = None,
|
|
280
291
|
keyword: str = "",
|
|
@@ -305,7 +316,7 @@ class RecordTools(ToolBase):
|
|
|
305
316
|
profile: str = DEFAULT_PROFILE,
|
|
306
317
|
app_key: str = "",
|
|
307
318
|
field_id: int = 0,
|
|
308
|
-
record_id:
|
|
319
|
+
record_id: str | None = None,
|
|
309
320
|
workflow_node_id: int | None = None,
|
|
310
321
|
fields: JSONObject | None = None,
|
|
311
322
|
keyword: str = "",
|
|
@@ -397,7 +408,7 @@ class RecordTools(ToolBase):
|
|
|
397
408
|
def record_get(
|
|
398
409
|
profile: str = DEFAULT_PROFILE,
|
|
399
410
|
app_key: str = "",
|
|
400
|
-
record_id:
|
|
411
|
+
record_id: str = "",
|
|
401
412
|
columns: list[JSONObject | int] | None = None,
|
|
402
413
|
view_id: str | None = None,
|
|
403
414
|
workflow_node_id: int | None = None,
|
|
@@ -429,7 +440,7 @@ class RecordTools(ToolBase):
|
|
|
429
440
|
@mcp.tool()
|
|
430
441
|
def record_update_schema_get(
|
|
431
442
|
app_key: str = "",
|
|
432
|
-
record_id:
|
|
443
|
+
record_id: str = "",
|
|
433
444
|
output_profile: str = "normal",
|
|
434
445
|
) -> JSONObject:
|
|
435
446
|
return self.record_update_schema_get_public(
|
|
@@ -469,7 +480,7 @@ class RecordTools(ToolBase):
|
|
|
469
480
|
)
|
|
470
481
|
def record_update(
|
|
471
482
|
app_key: str = "",
|
|
472
|
-
record_id:
|
|
483
|
+
record_id: str | None = None,
|
|
473
484
|
fields: JSONObject | None = None,
|
|
474
485
|
items: list[JSONObject] | None = None,
|
|
475
486
|
dry_run: bool = False,
|
|
@@ -495,8 +506,8 @@ class RecordTools(ToolBase):
|
|
|
495
506
|
)
|
|
496
507
|
def record_delete(
|
|
497
508
|
app_key: str = "",
|
|
498
|
-
record_id:
|
|
499
|
-
record_ids: list[
|
|
509
|
+
record_id: str | None = None,
|
|
510
|
+
record_ids: list[str] | None = None,
|
|
500
511
|
output_profile: str = "normal",
|
|
501
512
|
) -> JSONObject:
|
|
502
513
|
return self.record_delete_public(
|
|
@@ -774,14 +785,13 @@ class RecordTools(ToolBase):
|
|
|
774
785
|
*,
|
|
775
786
|
profile: str = DEFAULT_PROFILE,
|
|
776
787
|
app_key: str,
|
|
777
|
-
record_id:
|
|
788
|
+
record_id: Any,
|
|
778
789
|
output_profile: str = "normal",
|
|
779
790
|
) -> JSONObject:
|
|
780
791
|
"""执行记录相关逻辑。"""
|
|
781
792
|
if not app_key:
|
|
782
793
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
783
|
-
|
|
784
|
-
raise_tool_error(QingflowApiError.config_error("record_id is required"))
|
|
794
|
+
record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
|
|
785
795
|
normalized_output_profile = self._normalize_public_output_profile(output_profile)
|
|
786
796
|
|
|
787
797
|
def runner(session_profile, context):
|
|
@@ -801,55 +811,29 @@ class RecordTools(ToolBase):
|
|
|
801
811
|
index=app_index,
|
|
802
812
|
question_relations=question_relations,
|
|
803
813
|
)
|
|
804
|
-
try:
|
|
805
|
-
current_answers = self._load_record_answers_for_preflight(context, app_key=app_key, apply_id=record_id)
|
|
806
|
-
except QingflowApiError:
|
|
807
|
-
return self._record_update_schema_blocked_response(
|
|
808
|
-
profile=profile,
|
|
809
|
-
ws_id=session_profile.selected_ws_id,
|
|
810
|
-
request_route=request_route,
|
|
811
|
-
app_key=app_key,
|
|
812
|
-
record_id=record_id,
|
|
813
|
-
blockers=["CURRENT_RECORD_CONTEXT_UNAVAILABLE"],
|
|
814
|
-
warnings=[
|
|
815
|
-
"update schema could not load the current record; context-sensitive lookup requirements cannot be derived safely."
|
|
816
|
-
],
|
|
817
|
-
recommended_next_actions=[
|
|
818
|
-
"Retry after the record becomes readable in the current workspace/profile context.",
|
|
819
|
-
"If the issue persists, verify that the current profile still has read access to this record.",
|
|
820
|
-
],
|
|
821
|
-
output_profile=normalized_output_profile,
|
|
822
|
-
view_probe_summary=[],
|
|
823
|
-
ambiguous_fields=[],
|
|
824
|
-
)
|
|
825
|
-
|
|
826
814
|
candidate_routes = self._candidate_update_views(profile, context, app_key)
|
|
815
|
+
probes = self._probe_candidate_record_contexts(
|
|
816
|
+
context,
|
|
817
|
+
app_key=app_key,
|
|
818
|
+
apply_id=record_id_int,
|
|
819
|
+
candidate_routes=candidate_routes,
|
|
820
|
+
)
|
|
827
821
|
probe_summary: list[JSONObject] = []
|
|
828
|
-
|
|
822
|
+
matched_probes: list[RecordContextRouteProbe] = []
|
|
829
823
|
ordered_field_ids: list[int] = []
|
|
830
824
|
field_payloads_by_id: dict[int, JSONObject] = {}
|
|
831
825
|
title_to_field_ids: dict[str, list[int]] = {}
|
|
832
826
|
ws_id = session_profile.selected_ws_id
|
|
833
827
|
|
|
834
|
-
for
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
)
|
|
840
|
-
candidate_summary: JSONObject = {
|
|
841
|
-
"view_id": candidate.view_id,
|
|
842
|
-
"name": candidate.name,
|
|
843
|
-
"kind": candidate.kind,
|
|
844
|
-
"matched_record": matched_record,
|
|
845
|
-
"context_complete": True,
|
|
846
|
-
"writable_field_titles": [],
|
|
847
|
-
}
|
|
848
|
-
if not matched_record:
|
|
828
|
+
for probe in probes:
|
|
829
|
+
candidate = probe.route
|
|
830
|
+
candidate_summary = self._record_context_probe_summary_payload(probe)
|
|
831
|
+
candidate_summary["writable_field_titles"] = []
|
|
832
|
+
if not probe.readable:
|
|
849
833
|
probe_summary.append(candidate_summary)
|
|
850
834
|
continue
|
|
851
835
|
|
|
852
|
-
|
|
836
|
+
matched_probes.append(probe)
|
|
853
837
|
browse_scope = self._build_browse_write_scope(
|
|
854
838
|
profile,
|
|
855
839
|
context,
|
|
@@ -893,19 +877,39 @@ class RecordTools(ToolBase):
|
|
|
893
877
|
candidate_summary["writable_field_titles"] = candidate_titles
|
|
894
878
|
probe_summary.append(candidate_summary)
|
|
895
879
|
|
|
896
|
-
if not
|
|
880
|
+
if not matched_probes:
|
|
881
|
+
blockers = (
|
|
882
|
+
["CURRENT_RECORD_CONTEXT_UNAVAILABLE"]
|
|
883
|
+
if probes and all(probe.transport_error for probe in probes)
|
|
884
|
+
else ["NO_MATCHING_ACCESSIBLE_VIEW_FOR_RECORD"]
|
|
885
|
+
)
|
|
886
|
+
warnings = (
|
|
887
|
+
[
|
|
888
|
+
"update schema could not load the current record from any candidate route; context-sensitive lookup requirements cannot be derived safely."
|
|
889
|
+
]
|
|
890
|
+
if blockers == ["CURRENT_RECORD_CONTEXT_UNAVAILABLE"]
|
|
891
|
+
else []
|
|
892
|
+
)
|
|
893
|
+
recommended_next_actions = (
|
|
894
|
+
[
|
|
895
|
+
"Retry after the record becomes readable in the current workspace/profile context.",
|
|
896
|
+
"If the issue persists, verify that the current profile still has read access to this record.",
|
|
897
|
+
]
|
|
898
|
+
if blockers == ["CURRENT_RECORD_CONTEXT_UNAVAILABLE"]
|
|
899
|
+
else [
|
|
900
|
+
"Check whether this record is still visible in any accessible view for the current profile.",
|
|
901
|
+
"Use record_get or record_list to confirm the record still exists in the current workspace.",
|
|
902
|
+
]
|
|
903
|
+
)
|
|
897
904
|
return self._record_update_schema_blocked_response(
|
|
898
905
|
profile=profile,
|
|
899
906
|
ws_id=ws_id,
|
|
900
907
|
request_route=request_route,
|
|
901
908
|
app_key=app_key,
|
|
902
|
-
record_id=
|
|
903
|
-
blockers=
|
|
904
|
-
warnings=
|
|
905
|
-
recommended_next_actions=
|
|
906
|
-
"Check whether this record is still visible in any accessible view for the current profile.",
|
|
907
|
-
"Use record_get or record_list to confirm the record still exists in the current workspace.",
|
|
908
|
-
],
|
|
909
|
+
record_id=record_id_int,
|
|
910
|
+
blockers=blockers,
|
|
911
|
+
warnings=warnings,
|
|
912
|
+
recommended_next_actions=recommended_next_actions,
|
|
909
913
|
output_profile=normalized_output_profile,
|
|
910
914
|
view_probe_summary=probe_summary,
|
|
911
915
|
ambiguous_fields=[],
|
|
@@ -913,7 +917,10 @@ class RecordTools(ToolBase):
|
|
|
913
917
|
|
|
914
918
|
ambiguous_field_ids: set[int] = set()
|
|
915
919
|
ambiguous_fields: list[JSONObject] = []
|
|
916
|
-
warnings: list[JSONObject] = [
|
|
920
|
+
warnings: list[JSONObject] = [
|
|
921
|
+
{"code": "RECORD_CONTEXT_ROUTE_FALLBACK", "message": message}
|
|
922
|
+
for message in self._record_context_probe_fallback_warnings(probes)
|
|
923
|
+
]
|
|
917
924
|
for title, field_ids in title_to_field_ids.items():
|
|
918
925
|
unique_ids = list(dict.fromkeys(field_ids))
|
|
919
926
|
if len(unique_ids) <= 1:
|
|
@@ -942,7 +949,7 @@ class RecordTools(ToolBase):
|
|
|
942
949
|
ws_id=ws_id,
|
|
943
950
|
request_route=request_route,
|
|
944
951
|
app_key=app_key,
|
|
945
|
-
record_id=
|
|
952
|
+
record_id=record_id_int,
|
|
946
953
|
blockers=["NO_WRITABLE_FIELDS_FOR_RECORD"],
|
|
947
954
|
warnings=[item["message"] for item in warnings if isinstance(item.get("message"), str)],
|
|
948
955
|
recommended_next_actions=[
|
|
@@ -962,7 +969,7 @@ class RecordTools(ToolBase):
|
|
|
962
969
|
"request_route": request_route,
|
|
963
970
|
"warnings": warnings,
|
|
964
971
|
"app_key": app_key,
|
|
965
|
-
"record_id":
|
|
972
|
+
"record_id": stringify_backend_id(record_id_int),
|
|
966
973
|
"schema_scope": "update_ready",
|
|
967
974
|
"writable_fields": writable_fields,
|
|
968
975
|
"payload_template": {
|
|
@@ -972,6 +979,7 @@ class RecordTools(ToolBase):
|
|
|
972
979
|
}
|
|
973
980
|
if normalized_output_profile == "verbose":
|
|
974
981
|
response["view_probe_summary"] = probe_summary
|
|
982
|
+
response["record_context_probe"] = probe_summary
|
|
975
983
|
response["ambiguous_fields"] = ambiguous_fields
|
|
976
984
|
return response
|
|
977
985
|
|
|
@@ -1001,7 +1009,7 @@ class RecordTools(ToolBase):
|
|
|
1001
1009
|
"request_route": request_route,
|
|
1002
1010
|
"warnings": [{"code": "PREFLIGHT_WARNING", "message": item} for item in warnings],
|
|
1003
1011
|
"app_key": app_key,
|
|
1004
|
-
"record_id": record_id,
|
|
1012
|
+
"record_id": stringify_backend_id(record_id),
|
|
1005
1013
|
"schema_scope": "update_ready",
|
|
1006
1014
|
"blockers": blockers,
|
|
1007
1015
|
"writable_fields": [],
|
|
@@ -1022,6 +1030,7 @@ class RecordTools(ToolBase):
|
|
|
1022
1030
|
ws_id: int | None,
|
|
1023
1031
|
required_override: bool | None,
|
|
1024
1032
|
linkage_payloads_by_field_id: dict[str, JSONObject] | None = None,
|
|
1033
|
+
include_field_id: bool = True,
|
|
1025
1034
|
) -> JSONObject:
|
|
1026
1035
|
"""执行内部辅助逻辑。"""
|
|
1027
1036
|
kind = self._ready_schema_kind(field)
|
|
@@ -1042,6 +1051,8 @@ class RecordTools(ToolBase):
|
|
|
1042
1051
|
"kind": kind,
|
|
1043
1052
|
"required": required,
|
|
1044
1053
|
}
|
|
1054
|
+
if include_field_id:
|
|
1055
|
+
payload["field_id"] = field.que_id
|
|
1045
1056
|
if kind in {"single_select", "multi_select"} and field.options:
|
|
1046
1057
|
payload["options"] = list(field.options)
|
|
1047
1058
|
if kind in {"member", "department", "relation"}:
|
|
@@ -1097,6 +1108,7 @@ class RecordTools(ToolBase):
|
|
|
1097
1108
|
ws_id=ws_id,
|
|
1098
1109
|
required_override=True,
|
|
1099
1110
|
linkage_payloads_by_field_id=linkage_payloads_by_field_id,
|
|
1111
|
+
include_field_id=False,
|
|
1100
1112
|
)
|
|
1101
1113
|
payload["reason"] = reason
|
|
1102
1114
|
self._attach_ready_schema_linkage(
|
|
@@ -1177,6 +1189,7 @@ class RecordTools(ToolBase):
|
|
|
1177
1189
|
ws_id=ws_id,
|
|
1178
1190
|
required_override=bool(subfield.required),
|
|
1179
1191
|
linkage_payloads_by_field_id=linkage_payloads_by_field_id,
|
|
1192
|
+
include_field_id=False,
|
|
1180
1193
|
)
|
|
1181
1194
|
)
|
|
1182
1195
|
return payloads
|
|
@@ -1268,7 +1281,7 @@ class RecordTools(ToolBase):
|
|
|
1268
1281
|
profile: str,
|
|
1269
1282
|
app_key: str,
|
|
1270
1283
|
field_id: int,
|
|
1271
|
-
record_id:
|
|
1284
|
+
record_id: Any | None = None,
|
|
1272
1285
|
workflow_node_id: int | None = None,
|
|
1273
1286
|
fields: JSONObject | None = None,
|
|
1274
1287
|
keyword: str,
|
|
@@ -1284,6 +1297,12 @@ class RecordTools(ToolBase):
|
|
|
1284
1297
|
raise_tool_error(QingflowApiError.config_error("page_num must be positive"))
|
|
1285
1298
|
if page_size <= 0:
|
|
1286
1299
|
raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
|
|
1300
|
+
record_id_int = (
|
|
1301
|
+
normalize_positive_id_int(record_id, field_name="record_id")
|
|
1302
|
+
if record_id is not None
|
|
1303
|
+
else None
|
|
1304
|
+
)
|
|
1305
|
+
record_id_text = stringify_backend_id(record_id_int) if record_id_int is not None else None
|
|
1287
1306
|
|
|
1288
1307
|
def runner(session_profile, context):
|
|
1289
1308
|
index = self._get_field_index(profile, context, app_key, force_refresh=False)
|
|
@@ -1303,7 +1322,7 @@ class RecordTools(ToolBase):
|
|
|
1303
1322
|
)
|
|
1304
1323
|
normalized_fields = fields if isinstance(fields, dict) else {}
|
|
1305
1324
|
runtime_lookup = self._candidate_lookup_uses_runtime_scope(
|
|
1306
|
-
record_id=
|
|
1325
|
+
record_id=record_id_int,
|
|
1307
1326
|
workflow_node_id=workflow_node_id,
|
|
1308
1327
|
fields=normalized_fields,
|
|
1309
1328
|
)
|
|
@@ -1314,7 +1333,7 @@ class RecordTools(ToolBase):
|
|
|
1314
1333
|
profile,
|
|
1315
1334
|
context,
|
|
1316
1335
|
app_key=app_key,
|
|
1317
|
-
record_id=
|
|
1336
|
+
record_id=record_id_int,
|
|
1318
1337
|
workflow_node_id=workflow_node_id,
|
|
1319
1338
|
fields=normalized_fields,
|
|
1320
1339
|
)
|
|
@@ -1353,7 +1372,7 @@ class RecordTools(ToolBase):
|
|
|
1353
1372
|
"app_key": app_key,
|
|
1354
1373
|
"field_id": field.que_id,
|
|
1355
1374
|
"field_title": field.que_title,
|
|
1356
|
-
"record_id":
|
|
1375
|
+
"record_id": record_id_text,
|
|
1357
1376
|
"workflow_node_id": workflow_node_id,
|
|
1358
1377
|
"fields_present": bool(normalized_fields),
|
|
1359
1378
|
"keyword": keyword,
|
|
@@ -1372,7 +1391,7 @@ class RecordTools(ToolBase):
|
|
|
1372
1391
|
profile: str,
|
|
1373
1392
|
app_key: str,
|
|
1374
1393
|
field_id: int,
|
|
1375
|
-
record_id:
|
|
1394
|
+
record_id: Any | None = None,
|
|
1376
1395
|
workflow_node_id: int | None = None,
|
|
1377
1396
|
fields: JSONObject | None = None,
|
|
1378
1397
|
keyword: str,
|
|
@@ -1388,6 +1407,12 @@ class RecordTools(ToolBase):
|
|
|
1388
1407
|
raise_tool_error(QingflowApiError.config_error("page_num must be positive"))
|
|
1389
1408
|
if page_size <= 0:
|
|
1390
1409
|
raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
|
|
1410
|
+
record_id_int = (
|
|
1411
|
+
normalize_positive_id_int(record_id, field_name="record_id")
|
|
1412
|
+
if record_id is not None
|
|
1413
|
+
else None
|
|
1414
|
+
)
|
|
1415
|
+
record_id_text = stringify_backend_id(record_id_int) if record_id_int is not None else None
|
|
1391
1416
|
|
|
1392
1417
|
def runner(session_profile, context):
|
|
1393
1418
|
index = self._get_field_index(profile, context, app_key, force_refresh=False)
|
|
@@ -1407,7 +1432,7 @@ class RecordTools(ToolBase):
|
|
|
1407
1432
|
)
|
|
1408
1433
|
normalized_fields = fields if isinstance(fields, dict) else {}
|
|
1409
1434
|
runtime_lookup = self._candidate_lookup_uses_runtime_scope(
|
|
1410
|
-
record_id=
|
|
1435
|
+
record_id=record_id_int,
|
|
1411
1436
|
workflow_node_id=workflow_node_id,
|
|
1412
1437
|
fields=normalized_fields,
|
|
1413
1438
|
)
|
|
@@ -1418,7 +1443,7 @@ class RecordTools(ToolBase):
|
|
|
1418
1443
|
profile,
|
|
1419
1444
|
context,
|
|
1420
1445
|
app_key=app_key,
|
|
1421
|
-
record_id=
|
|
1446
|
+
record_id=record_id_int,
|
|
1422
1447
|
workflow_node_id=workflow_node_id,
|
|
1423
1448
|
fields=normalized_fields,
|
|
1424
1449
|
)
|
|
@@ -1474,7 +1499,7 @@ class RecordTools(ToolBase):
|
|
|
1474
1499
|
"app_key": app_key,
|
|
1475
1500
|
"field_id": field.que_id,
|
|
1476
1501
|
"field_title": field.que_title,
|
|
1477
|
-
"record_id":
|
|
1502
|
+
"record_id": record_id_text,
|
|
1478
1503
|
"workflow_node_id": workflow_node_id,
|
|
1479
1504
|
"fields_present": bool(normalized_fields),
|
|
1480
1505
|
"keyword": keyword,
|
|
@@ -1740,7 +1765,7 @@ class RecordTools(ToolBase):
|
|
|
1740
1765
|
*,
|
|
1741
1766
|
profile: str,
|
|
1742
1767
|
app_key: str,
|
|
1743
|
-
record_id:
|
|
1768
|
+
record_id: Any,
|
|
1744
1769
|
columns: list[JSONObject | int],
|
|
1745
1770
|
view_id: str | None = None,
|
|
1746
1771
|
workflow_node_id: int | None = None,
|
|
@@ -1748,8 +1773,7 @@ class RecordTools(ToolBase):
|
|
|
1748
1773
|
) -> JSONObject:
|
|
1749
1774
|
"""执行记录相关逻辑。"""
|
|
1750
1775
|
normalized_output_profile = self._normalize_public_output_profile(output_profile, allow_normalized=True)
|
|
1751
|
-
|
|
1752
|
-
raise_tool_error(QingflowApiError.config_error("record_id must be positive"))
|
|
1776
|
+
record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
|
|
1753
1777
|
normalized_columns = _normalize_public_column_selectors(columns)
|
|
1754
1778
|
|
|
1755
1779
|
def runner(session_profile, context):
|
|
@@ -1778,7 +1802,7 @@ class RecordTools(ToolBase):
|
|
|
1778
1802
|
result = self.backend.request(
|
|
1779
1803
|
"GET",
|
|
1780
1804
|
context,
|
|
1781
|
-
f"/view/{resolved_view.view_selection.view_key}/apply/{
|
|
1805
|
+
f"/view/{resolved_view.view_selection.view_key}/apply/{record_id_int}",
|
|
1782
1806
|
)
|
|
1783
1807
|
used_list_type = None
|
|
1784
1808
|
else:
|
|
@@ -1795,7 +1819,7 @@ class RecordTools(ToolBase):
|
|
|
1795
1819
|
result = self.backend.request(
|
|
1796
1820
|
"GET",
|
|
1797
1821
|
context,
|
|
1798
|
-
f"/app/{app_key}/apply/{
|
|
1822
|
+
f"/app/{app_key}/apply/{record_id_int}",
|
|
1799
1823
|
params={"role": 1, "listType": lt},
|
|
1800
1824
|
)
|
|
1801
1825
|
used_list_type = lt
|
|
@@ -1810,7 +1834,7 @@ class RecordTools(ToolBase):
|
|
|
1810
1834
|
raise last_error
|
|
1811
1835
|
raise_tool_error(QingflowApiError.config_error("record_get failed: no accessible listType"))
|
|
1812
1836
|
answer_list = result.get("answers") if isinstance(result, dict) and isinstance(result.get("answers"), list) else []
|
|
1813
|
-
row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=
|
|
1837
|
+
row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id_int)
|
|
1814
1838
|
normalized_record, normalized_ambiguous_fields = _build_normalized_row_from_answers(
|
|
1815
1839
|
cast(list[JSONValue], answer_list),
|
|
1816
1840
|
selected_fields,
|
|
@@ -1838,7 +1862,7 @@ class RecordTools(ToolBase):
|
|
|
1838
1862
|
if normalized_columns
|
|
1839
1863
|
else list(index.by_id.values())
|
|
1840
1864
|
)
|
|
1841
|
-
row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=
|
|
1865
|
+
row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id_int)
|
|
1842
1866
|
normalized_record, normalized_ambiguous_fields = _build_normalized_row_from_answers(
|
|
1843
1867
|
cast(list[JSONValue], answer_list),
|
|
1844
1868
|
selected_fields,
|
|
@@ -1861,7 +1885,7 @@ class RecordTools(ToolBase):
|
|
|
1861
1885
|
"output_profile": normalized_output_profile,
|
|
1862
1886
|
"data": {
|
|
1863
1887
|
"app_key": app_key,
|
|
1864
|
-
"record_id": _public_record_id_text(
|
|
1888
|
+
"record_id": _public_record_id_text(record_id_int),
|
|
1865
1889
|
"record": row,
|
|
1866
1890
|
"selection": {
|
|
1867
1891
|
"columns": [_column_selector_payload(field_id) for field_id in normalized_columns] if normalized_columns else [],
|
|
@@ -1962,7 +1986,7 @@ class RecordTools(ToolBase):
|
|
|
1962
1986
|
*,
|
|
1963
1987
|
profile: str = DEFAULT_PROFILE,
|
|
1964
1988
|
app_key: str,
|
|
1965
|
-
record_id:
|
|
1989
|
+
record_id: Any | None,
|
|
1966
1990
|
fields: JSONObject | None = None,
|
|
1967
1991
|
items: list[JSONObject] | None = None,
|
|
1968
1992
|
dry_run: bool = False,
|
|
@@ -1991,14 +2015,15 @@ class RecordTools(ToolBase):
|
|
|
1991
2015
|
)
|
|
1992
2016
|
if dry_run:
|
|
1993
2017
|
raise_tool_error(QingflowApiError.config_error("dry_run currently requires items"))
|
|
1994
|
-
if record_id is None
|
|
2018
|
+
if record_id is None:
|
|
1995
2019
|
raise_tool_error(QingflowApiError.config_error("record_id is required"))
|
|
2020
|
+
record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
|
|
1996
2021
|
if fields is not None and not isinstance(fields, dict):
|
|
1997
2022
|
raise_tool_error(QingflowApiError.config_error("fields must be an object map keyed by field title"))
|
|
1998
2023
|
return self._record_update_public_single(
|
|
1999
2024
|
profile=profile,
|
|
2000
2025
|
app_key=app_key,
|
|
2001
|
-
record_id=
|
|
2026
|
+
record_id=record_id_int,
|
|
2002
2027
|
fields=cast(JSONObject, fields or {}),
|
|
2003
2028
|
verify_write=verify_write,
|
|
2004
2029
|
output_profile=normalized_output_profile,
|
|
@@ -2188,7 +2213,7 @@ class RecordTools(ToolBase):
|
|
|
2188
2213
|
def _normalize_public_record_update_batch_items(
|
|
2189
2214
|
self,
|
|
2190
2215
|
*,
|
|
2191
|
-
record_id:
|
|
2216
|
+
record_id: Any | None,
|
|
2192
2217
|
fields: JSONObject | None,
|
|
2193
2218
|
items: list[JSONObject] | None,
|
|
2194
2219
|
) -> list[JSONObject]:
|
|
@@ -2204,9 +2229,10 @@ class RecordTools(ToolBase):
|
|
|
2204
2229
|
for index, item in enumerate(items):
|
|
2205
2230
|
if not isinstance(item, dict):
|
|
2206
2231
|
raise_tool_error(QingflowApiError.config_error(f"items[{index}] must be an object"))
|
|
2207
|
-
normalized_record_id =
|
|
2208
|
-
|
|
2209
|
-
|
|
2232
|
+
normalized_record_id = normalize_positive_id_int(
|
|
2233
|
+
item.get("record_id"),
|
|
2234
|
+
field_name=f"items[{index}].record_id",
|
|
2235
|
+
)
|
|
2210
2236
|
if normalized_record_id in seen_record_ids:
|
|
2211
2237
|
raise_tool_error(
|
|
2212
2238
|
QingflowApiError.config_error(f"duplicate record_id in items: {normalized_record_id}")
|
|
@@ -2336,9 +2362,15 @@ class RecordTools(ToolBase):
|
|
|
2336
2362
|
def runner(session_profile, context):
|
|
2337
2363
|
request_route = self._request_route_payload(context)
|
|
2338
2364
|
def build_once(*, effective_force_refresh: bool) -> JSONObject:
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2365
|
+
candidate_routes = self._candidate_update_views(profile, context, app_key)
|
|
2366
|
+
probes = self._probe_candidate_record_contexts(
|
|
2367
|
+
context,
|
|
2368
|
+
app_key=app_key,
|
|
2369
|
+
apply_id=record_id,
|
|
2370
|
+
candidate_routes=candidate_routes,
|
|
2371
|
+
)
|
|
2372
|
+
matched_probes = [probe for probe in probes if probe.readable]
|
|
2373
|
+
if not matched_probes and probes and all(probe.transport_error for probe in probes):
|
|
2342
2374
|
return {
|
|
2343
2375
|
"profile": profile,
|
|
2344
2376
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -2349,46 +2381,40 @@ class RecordTools(ToolBase):
|
|
|
2349
2381
|
record_id=record_id,
|
|
2350
2382
|
blockers=["CURRENT_RECORD_CONTEXT_UNAVAILABLE"],
|
|
2351
2383
|
warnings=[
|
|
2352
|
-
"update preflight could not load the current record; automatic view selection was blocked to avoid resolving lookup fields against a partial patch."
|
|
2384
|
+
"update preflight could not load the current record from any candidate route; automatic view selection was blocked to avoid resolving lookup fields against a partial patch."
|
|
2353
2385
|
],
|
|
2354
2386
|
recommended_next_actions=[
|
|
2355
2387
|
"Retry after the record becomes readable in the current workspace/profile context.",
|
|
2356
2388
|
"Call record_update_schema_get to inspect the overall writable field set for this record after context access is restored.",
|
|
2357
2389
|
],
|
|
2358
|
-
view_probe_summary=[
|
|
2390
|
+
view_probe_summary=[
|
|
2391
|
+
self._record_context_probe_summary_payload(probe)
|
|
2392
|
+
for probe in probes
|
|
2393
|
+
],
|
|
2359
2394
|
),
|
|
2360
2395
|
}
|
|
2361
2396
|
|
|
2362
|
-
candidate_routes = self._candidate_update_views(profile, context, app_key)
|
|
2363
2397
|
probe_summary: list[JSONObject] = []
|
|
2364
|
-
matched_any = False
|
|
2365
2398
|
matched_routes: list[AccessibleViewRoute] = []
|
|
2399
|
+
matched_answers_for_union: list[JSONObject] | None = None
|
|
2366
2400
|
first_blocked_plan: JSONObject | None = None
|
|
2367
2401
|
first_confirmation_plan: JSONObject | None = None
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
"view_id": candidate.view_id,
|
|
2379
|
-
"name": candidate.name,
|
|
2380
|
-
"kind": candidate.kind,
|
|
2381
|
-
"matched_record": False,
|
|
2382
|
-
"writable_field_titles": [],
|
|
2383
|
-
"missing_field_titles": [],
|
|
2384
|
-
"context_complete": True,
|
|
2385
|
-
"selected": False,
|
|
2386
|
-
}
|
|
2387
|
-
)
|
|
2402
|
+
fallback_warning_messages = self._record_context_probe_fallback_warnings(probes)
|
|
2403
|
+
|
|
2404
|
+
for probe in probes:
|
|
2405
|
+
candidate = probe.route
|
|
2406
|
+
candidate_summary = self._record_context_probe_summary_payload(probe)
|
|
2407
|
+
candidate_summary["writable_field_titles"] = []
|
|
2408
|
+
candidate_summary["missing_field_titles"] = []
|
|
2409
|
+
candidate_summary["selected"] = False
|
|
2410
|
+
if not probe.readable:
|
|
2411
|
+
probe_summary.append(candidate_summary)
|
|
2388
2412
|
continue
|
|
2389
2413
|
|
|
2390
|
-
|
|
2414
|
+
current_answers = probe.answer_list or []
|
|
2391
2415
|
matched_routes.append(candidate)
|
|
2416
|
+
if matched_answers_for_union is None:
|
|
2417
|
+
matched_answers_for_union = current_answers
|
|
2392
2418
|
browse_scope = self._build_browse_write_scope(
|
|
2393
2419
|
profile,
|
|
2394
2420
|
context,
|
|
@@ -2444,16 +2470,8 @@ class RecordTools(ToolBase):
|
|
|
2444
2470
|
title = _normalize_optional_text(field_payload.get("que_title"))
|
|
2445
2471
|
if title and title not in missing_field_titles:
|
|
2446
2472
|
missing_field_titles.append(title)
|
|
2447
|
-
candidate_summary
|
|
2448
|
-
|
|
2449
|
-
"name": candidate.name,
|
|
2450
|
-
"kind": candidate.kind,
|
|
2451
|
-
"matched_record": True,
|
|
2452
|
-
"writable_field_titles": candidate_titles,
|
|
2453
|
-
"missing_field_titles": missing_field_titles,
|
|
2454
|
-
"context_complete": True,
|
|
2455
|
-
"selected": False,
|
|
2456
|
-
}
|
|
2473
|
+
candidate_summary["writable_field_titles"] = candidate_titles
|
|
2474
|
+
candidate_summary["missing_field_titles"] = missing_field_titles
|
|
2457
2475
|
if plan_data.get("blockers"):
|
|
2458
2476
|
confirmation_requests = plan_data.get("confirmation_requests")
|
|
2459
2477
|
if (
|
|
@@ -2472,8 +2490,26 @@ class RecordTools(ToolBase):
|
|
|
2472
2490
|
view_payload["auto_selected"] = True
|
|
2473
2491
|
view_payload["selection_source"] = "first_satisfying_accessible_view"
|
|
2474
2492
|
candidate_summary["selected"] = True
|
|
2493
|
+
validation = plan_data.get("validation")
|
|
2494
|
+
if isinstance(validation, dict):
|
|
2495
|
+
warnings = validation.get("warnings")
|
|
2496
|
+
if not isinstance(warnings, list):
|
|
2497
|
+
warnings = []
|
|
2498
|
+
validation["warnings"] = warnings
|
|
2499
|
+
for message in fallback_warning_messages:
|
|
2500
|
+
if message not in warnings:
|
|
2501
|
+
warnings.append(message)
|
|
2475
2502
|
first_confirmation_plan = plan_data
|
|
2476
2503
|
elif first_blocked_plan is None:
|
|
2504
|
+
validation = plan_data.get("validation")
|
|
2505
|
+
if isinstance(validation, dict):
|
|
2506
|
+
warnings = validation.get("warnings")
|
|
2507
|
+
if not isinstance(warnings, list):
|
|
2508
|
+
warnings = []
|
|
2509
|
+
validation["warnings"] = warnings
|
|
2510
|
+
for message in fallback_warning_messages:
|
|
2511
|
+
if message not in warnings:
|
|
2512
|
+
warnings.append(message)
|
|
2477
2513
|
first_blocked_plan = plan_data
|
|
2478
2514
|
probe_summary.append(candidate_summary)
|
|
2479
2515
|
continue
|
|
@@ -2489,8 +2525,18 @@ class RecordTools(ToolBase):
|
|
|
2489
2525
|
view_payload["auto_selected"] = True
|
|
2490
2526
|
view_payload["selection_source"] = "first_satisfying_accessible_view"
|
|
2491
2527
|
candidate_summary["selected"] = True
|
|
2528
|
+
validation = plan_data.get("validation")
|
|
2529
|
+
if isinstance(validation, dict):
|
|
2530
|
+
warnings = validation.get("warnings")
|
|
2531
|
+
if not isinstance(warnings, list):
|
|
2532
|
+
warnings = []
|
|
2533
|
+
validation["warnings"] = warnings
|
|
2534
|
+
for message in fallback_warning_messages:
|
|
2535
|
+
if message not in warnings:
|
|
2536
|
+
warnings.append(message)
|
|
2492
2537
|
probe_summary.append(candidate_summary)
|
|
2493
2538
|
plan_data["view_probe_summary"] = probe_summary
|
|
2539
|
+
plan_data["record_context_probe"] = probe_summary
|
|
2494
2540
|
return {
|
|
2495
2541
|
"profile": profile,
|
|
2496
2542
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -2499,7 +2545,7 @@ class RecordTools(ToolBase):
|
|
|
2499
2545
|
"data": plan_data,
|
|
2500
2546
|
}
|
|
2501
2547
|
|
|
2502
|
-
if not
|
|
2548
|
+
if not matched_probes:
|
|
2503
2549
|
blocked_data = self._build_auto_view_blocked_preflight_data(
|
|
2504
2550
|
app_key=app_key,
|
|
2505
2551
|
record_id=record_id,
|
|
@@ -2521,6 +2567,7 @@ class RecordTools(ToolBase):
|
|
|
2521
2567
|
|
|
2522
2568
|
if first_confirmation_plan is not None:
|
|
2523
2569
|
first_confirmation_plan["view_probe_summary"] = probe_summary
|
|
2570
|
+
first_confirmation_plan["record_context_probe"] = probe_summary
|
|
2524
2571
|
return {
|
|
2525
2572
|
"profile": profile,
|
|
2526
2573
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -2535,12 +2582,22 @@ class RecordTools(ToolBase):
|
|
|
2535
2582
|
app_key=app_key,
|
|
2536
2583
|
record_id=record_id,
|
|
2537
2584
|
fields=fields,
|
|
2538
|
-
current_answers=
|
|
2585
|
+
current_answers=matched_answers_for_union or [],
|
|
2539
2586
|
matched_routes=matched_routes,
|
|
2540
2587
|
force_refresh_form=effective_force_refresh,
|
|
2541
2588
|
)
|
|
2542
2589
|
if union_plan is not None:
|
|
2590
|
+
validation = union_plan.get("validation")
|
|
2591
|
+
if isinstance(validation, dict):
|
|
2592
|
+
warnings = validation.get("warnings")
|
|
2593
|
+
if not isinstance(warnings, list):
|
|
2594
|
+
warnings = []
|
|
2595
|
+
validation["warnings"] = warnings
|
|
2596
|
+
for message in fallback_warning_messages:
|
|
2597
|
+
if message not in warnings:
|
|
2598
|
+
warnings.append(message)
|
|
2543
2599
|
union_plan["view_probe_summary"] = probe_summary
|
|
2600
|
+
union_plan["record_context_probe"] = probe_summary
|
|
2544
2601
|
return {
|
|
2545
2602
|
"profile": profile,
|
|
2546
2603
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -2803,8 +2860,6 @@ class RecordTools(ToolBase):
|
|
|
2803
2860
|
"""执行内部辅助逻辑。"""
|
|
2804
2861
|
candidates: list[AccessibleViewRoute] = []
|
|
2805
2862
|
for view_id, list_type, name in SYSTEM_VIEW_DEFINITIONS:
|
|
2806
|
-
if not self._probe_list_type_access(context, app_key=app_key, list_type=list_type):
|
|
2807
|
-
continue
|
|
2808
2863
|
candidates.append(
|
|
2809
2864
|
AccessibleViewRoute(
|
|
2810
2865
|
view_id=view_id,
|
|
@@ -2821,11 +2876,22 @@ class RecordTools(ToolBase):
|
|
|
2821
2876
|
view_key = _normalize_optional_text(item.get("viewKey"))
|
|
2822
2877
|
if not view_key:
|
|
2823
2878
|
continue
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2879
|
+
view_name = _normalize_optional_text(item.get("viewName")) or view_key
|
|
2880
|
+
view_type = _normalize_optional_text(item.get("viewType") or item.get("viewgraphType"))
|
|
2881
|
+
filter_config: JSONObject | None = None
|
|
2882
|
+
if isinstance(item.get("viewgraphLimit"), list):
|
|
2883
|
+
filter_config = item
|
|
2884
|
+
else:
|
|
2885
|
+
raw_view_config = item.get("viewConfig")
|
|
2886
|
+
if isinstance(raw_view_config, dict):
|
|
2887
|
+
filter_config = raw_view_config
|
|
2888
|
+
view_selection = ViewSelection(
|
|
2889
|
+
view_key=view_key,
|
|
2890
|
+
view_name=view_name,
|
|
2891
|
+
conditions=_compile_view_conditions(filter_config or {}),
|
|
2892
|
+
view_type=view_type,
|
|
2893
|
+
filter_config_loaded=isinstance(filter_config, dict),
|
|
2894
|
+
)
|
|
2829
2895
|
candidates.append(
|
|
2830
2896
|
AccessibleViewRoute(
|
|
2831
2897
|
view_id=f"custom:{view_key}",
|
|
@@ -2838,6 +2904,264 @@ class RecordTools(ToolBase):
|
|
|
2838
2904
|
)
|
|
2839
2905
|
return candidates
|
|
2840
2906
|
|
|
2907
|
+
def _route_view_key(self, resolved_view: AccessibleViewRoute) -> str | None:
|
|
2908
|
+
if resolved_view.view_selection is not None:
|
|
2909
|
+
view_key = _normalize_optional_text(resolved_view.view_selection.view_key)
|
|
2910
|
+
if view_key:
|
|
2911
|
+
return view_key
|
|
2912
|
+
if resolved_view.kind == "custom" and resolved_view.view_id.startswith("custom:"):
|
|
2913
|
+
view_key = resolved_view.view_id.split(":", 1)[1].strip()
|
|
2914
|
+
return view_key or None
|
|
2915
|
+
return None
|
|
2916
|
+
|
|
2917
|
+
def _record_context_route_error_payload(self, error: QingflowApiError) -> JSONObject:
|
|
2918
|
+
payload: JSONObject = {"message": error.message}
|
|
2919
|
+
if error.category:
|
|
2920
|
+
payload["category"] = error.category
|
|
2921
|
+
if error.backend_code is not None:
|
|
2922
|
+
payload["backend_code"] = error.backend_code
|
|
2923
|
+
if error.request_id is not None:
|
|
2924
|
+
payload["request_id"] = error.request_id
|
|
2925
|
+
if error.http_status is not None:
|
|
2926
|
+
payload["http_status"] = error.http_status
|
|
2927
|
+
return payload
|
|
2928
|
+
|
|
2929
|
+
def _is_record_context_route_miss(self, error: QingflowApiError) -> bool:
|
|
2930
|
+
if error.backend_code in {40002, 40023, 40027, 40038, 404}:
|
|
2931
|
+
return True
|
|
2932
|
+
if error.http_status == 404:
|
|
2933
|
+
return True
|
|
2934
|
+
return False
|
|
2935
|
+
|
|
2936
|
+
def _load_record_answers_for_accessible_route(
|
|
2937
|
+
self,
|
|
2938
|
+
context, # type: ignore[no-untyped-def]
|
|
2939
|
+
*,
|
|
2940
|
+
app_key: str,
|
|
2941
|
+
apply_id: int,
|
|
2942
|
+
resolved_view: AccessibleViewRoute,
|
|
2943
|
+
) -> tuple[list[JSONObject], int | None]:
|
|
2944
|
+
if resolved_view.kind == "custom":
|
|
2945
|
+
view_key = self._route_view_key(resolved_view)
|
|
2946
|
+
if not view_key:
|
|
2947
|
+
raise_tool_error(
|
|
2948
|
+
QingflowApiError.config_error(
|
|
2949
|
+
f"cannot resolve custom view route for '{resolved_view.view_id}'"
|
|
2950
|
+
)
|
|
2951
|
+
)
|
|
2952
|
+
record = self.backend.request(
|
|
2953
|
+
"GET",
|
|
2954
|
+
context,
|
|
2955
|
+
f"/view/{view_key}/apply/{apply_id}",
|
|
2956
|
+
)
|
|
2957
|
+
used_list_type = None
|
|
2958
|
+
else:
|
|
2959
|
+
used_list_type = resolved_view.list_type if resolved_view.list_type is not None else DEFAULT_RECORD_LIST_TYPE
|
|
2960
|
+
record = self.backend.request(
|
|
2961
|
+
"GET",
|
|
2962
|
+
context,
|
|
2963
|
+
f"/app/{app_key}/apply/{apply_id}",
|
|
2964
|
+
params={"role": 1, "listType": used_list_type},
|
|
2965
|
+
)
|
|
2966
|
+
answers = record.get("answers") if isinstance(record, dict) else None
|
|
2967
|
+
normalized_answers = [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []
|
|
2968
|
+
return normalized_answers, used_list_type
|
|
2969
|
+
|
|
2970
|
+
def _probe_record_context_route(
|
|
2971
|
+
self,
|
|
2972
|
+
context, # type: ignore[no-untyped-def]
|
|
2973
|
+
*,
|
|
2974
|
+
app_key: str,
|
|
2975
|
+
apply_id: int,
|
|
2976
|
+
resolved_view: AccessibleViewRoute,
|
|
2977
|
+
) -> RecordContextRouteProbe:
|
|
2978
|
+
try:
|
|
2979
|
+
answer_list, used_list_type = self._load_record_answers_for_accessible_route(
|
|
2980
|
+
context,
|
|
2981
|
+
app_key=app_key,
|
|
2982
|
+
apply_id=apply_id,
|
|
2983
|
+
resolved_view=resolved_view,
|
|
2984
|
+
)
|
|
2985
|
+
return RecordContextRouteProbe(
|
|
2986
|
+
route=resolved_view,
|
|
2987
|
+
answer_list=answer_list,
|
|
2988
|
+
used_list_type=used_list_type,
|
|
2989
|
+
readable=True,
|
|
2990
|
+
transport_error=False,
|
|
2991
|
+
error_payload=None,
|
|
2992
|
+
)
|
|
2993
|
+
except QingflowApiError as exc:
|
|
2994
|
+
return RecordContextRouteProbe(
|
|
2995
|
+
route=resolved_view,
|
|
2996
|
+
answer_list=None,
|
|
2997
|
+
used_list_type=resolved_view.list_type if resolved_view.kind == "system" else None,
|
|
2998
|
+
readable=False,
|
|
2999
|
+
transport_error=not self._is_record_context_route_miss(exc),
|
|
3000
|
+
error_payload=self._record_context_route_error_payload(exc),
|
|
3001
|
+
)
|
|
3002
|
+
|
|
3003
|
+
def _probe_candidate_record_contexts(
|
|
3004
|
+
self,
|
|
3005
|
+
context, # type: ignore[no-untyped-def]
|
|
3006
|
+
*,
|
|
3007
|
+
app_key: str,
|
|
3008
|
+
apply_id: int,
|
|
3009
|
+
candidate_routes: list[AccessibleViewRoute],
|
|
3010
|
+
) -> list[RecordContextRouteProbe]:
|
|
3011
|
+
return [
|
|
3012
|
+
self._probe_record_context_route(
|
|
3013
|
+
context,
|
|
3014
|
+
app_key=app_key,
|
|
3015
|
+
apply_id=apply_id,
|
|
3016
|
+
resolved_view=candidate,
|
|
3017
|
+
)
|
|
3018
|
+
for candidate in candidate_routes
|
|
3019
|
+
]
|
|
3020
|
+
|
|
3021
|
+
def _record_context_probe_summary_payload(self, probe: RecordContextRouteProbe) -> JSONObject:
|
|
3022
|
+
payload: JSONObject = {
|
|
3023
|
+
"view_id": probe.route.view_id,
|
|
3024
|
+
"name": probe.route.name,
|
|
3025
|
+
"kind": probe.route.kind,
|
|
3026
|
+
"matched_record": probe.readable,
|
|
3027
|
+
"readable": probe.readable,
|
|
3028
|
+
"context_complete": probe.readable,
|
|
3029
|
+
"used_list_type": probe.used_list_type,
|
|
3030
|
+
}
|
|
3031
|
+
if probe.error_payload is not None:
|
|
3032
|
+
payload["error"] = probe.error_payload
|
|
3033
|
+
payload["transport_error"] = probe.transport_error
|
|
3034
|
+
return payload
|
|
3035
|
+
|
|
3036
|
+
def _record_context_probe_fallback_warnings(
|
|
3037
|
+
self,
|
|
3038
|
+
probes: list[RecordContextRouteProbe],
|
|
3039
|
+
) -> list[str]:
|
|
3040
|
+
matched_probes = [probe for probe in probes if probe.readable]
|
|
3041
|
+
if not matched_probes:
|
|
3042
|
+
return []
|
|
3043
|
+
if any(
|
|
3044
|
+
probe.route.kind == "system" and probe.used_list_type == DEFAULT_RECORD_LIST_TYPE
|
|
3045
|
+
for probe in matched_probes
|
|
3046
|
+
):
|
|
3047
|
+
return []
|
|
3048
|
+
first_probe = matched_probes[0]
|
|
3049
|
+
if first_probe.route.kind == "custom":
|
|
3050
|
+
return [
|
|
3051
|
+
"current record context was not accessible via listType="
|
|
3052
|
+
f"{DEFAULT_RECORD_LIST_TYPE}; preflight resolved it via custom view "
|
|
3053
|
+
f"'{first_probe.route.name}'."
|
|
3054
|
+
]
|
|
3055
|
+
used_list_type = first_probe.used_list_type
|
|
3056
|
+
if used_list_type is None:
|
|
3057
|
+
return []
|
|
3058
|
+
return [
|
|
3059
|
+
"current record context was not accessible via listType="
|
|
3060
|
+
f"{DEFAULT_RECORD_LIST_TYPE}; preflight resolved it via listType={used_list_type} "
|
|
3061
|
+
f"({get_record_list_type_label(used_list_type)})."
|
|
3062
|
+
]
|
|
3063
|
+
|
|
3064
|
+
def _looks_like_generic_record_update_backend_failure(self, exc: QingflowApiError) -> bool:
|
|
3065
|
+
if exc.backend_code == 500:
|
|
3066
|
+
return True
|
|
3067
|
+
if exc.http_status is not None and exc.http_status >= 500:
|
|
3068
|
+
return True
|
|
3069
|
+
normalized_message = exc.message.strip().lower()
|
|
3070
|
+
return normalized_message in {"unknown error", "internal server error"}
|
|
3071
|
+
|
|
3072
|
+
def _remap_record_update_target_context_error(
|
|
3073
|
+
self,
|
|
3074
|
+
profile: str,
|
|
3075
|
+
context, # type: ignore[no-untyped-def]
|
|
3076
|
+
*,
|
|
3077
|
+
app_key: str,
|
|
3078
|
+
apply_id: int,
|
|
3079
|
+
exc: QingflowApiError,
|
|
3080
|
+
) -> None:
|
|
3081
|
+
if not self._looks_like_generic_record_update_backend_failure(exc):
|
|
3082
|
+
return
|
|
3083
|
+
try:
|
|
3084
|
+
candidate_routes = self._candidate_update_views(profile, context, app_key)
|
|
3085
|
+
probes = self._probe_candidate_record_contexts(
|
|
3086
|
+
context,
|
|
3087
|
+
app_key=app_key,
|
|
3088
|
+
apply_id=apply_id,
|
|
3089
|
+
candidate_routes=candidate_routes,
|
|
3090
|
+
)
|
|
3091
|
+
except (QingflowApiError, RuntimeError):
|
|
3092
|
+
return
|
|
3093
|
+
if not probes or any(probe.readable for probe in probes):
|
|
3094
|
+
return
|
|
3095
|
+
|
|
3096
|
+
blocker = (
|
|
3097
|
+
"CURRENT_RECORD_CONTEXT_UNAVAILABLE"
|
|
3098
|
+
if all(probe.transport_error for probe in probes)
|
|
3099
|
+
else "NO_MATCHING_ACCESSIBLE_VIEW_FOR_RECORD"
|
|
3100
|
+
)
|
|
3101
|
+
recommended_next_actions = (
|
|
3102
|
+
[
|
|
3103
|
+
"Retry after the record becomes readable in the current workspace/profile context.",
|
|
3104
|
+
"If the issue persists, verify that the current profile still has read access to this record.",
|
|
3105
|
+
]
|
|
3106
|
+
if blocker == "CURRENT_RECORD_CONTEXT_UNAVAILABLE"
|
|
3107
|
+
else [
|
|
3108
|
+
"Use record_get or record_list to confirm the record still exists in the current workspace.",
|
|
3109
|
+
"Call record_update_schema_get to inspect whether any accessible view still matches this record.",
|
|
3110
|
+
]
|
|
3111
|
+
)
|
|
3112
|
+
first_error_payload = next(
|
|
3113
|
+
(
|
|
3114
|
+
cast(JSONObject, probe.error_payload)
|
|
3115
|
+
for probe in probes
|
|
3116
|
+
if isinstance(probe.error_payload, dict)
|
|
3117
|
+
),
|
|
3118
|
+
None,
|
|
3119
|
+
)
|
|
3120
|
+
backend_code = (
|
|
3121
|
+
cast(int, first_error_payload.get("backend_code"))
|
|
3122
|
+
if isinstance(first_error_payload, dict) and isinstance(first_error_payload.get("backend_code"), int)
|
|
3123
|
+
else exc.backend_code
|
|
3124
|
+
)
|
|
3125
|
+
request_id = (
|
|
3126
|
+
_normalize_optional_text(first_error_payload.get("request_id"))
|
|
3127
|
+
if isinstance(first_error_payload, dict)
|
|
3128
|
+
else None
|
|
3129
|
+
) or exc.request_id
|
|
3130
|
+
http_status = (
|
|
3131
|
+
cast(int, first_error_payload.get("http_status"))
|
|
3132
|
+
if isinstance(first_error_payload, dict) and isinstance(first_error_payload.get("http_status"), int)
|
|
3133
|
+
else exc.http_status
|
|
3134
|
+
)
|
|
3135
|
+
raise_tool_error(
|
|
3136
|
+
QingflowApiError(
|
|
3137
|
+
category="backend",
|
|
3138
|
+
message=(
|
|
3139
|
+
"Direct record edit was blocked because the current record context could not be loaded from any candidate route."
|
|
3140
|
+
if blocker == "CURRENT_RECORD_CONTEXT_UNAVAILABLE"
|
|
3141
|
+
else "Direct record edit was blocked because the target record is no longer accessible in any readable view for the current profile."
|
|
3142
|
+
),
|
|
3143
|
+
backend_code=backend_code,
|
|
3144
|
+
request_id=request_id,
|
|
3145
|
+
http_status=http_status,
|
|
3146
|
+
details={
|
|
3147
|
+
"error_code": blocker,
|
|
3148
|
+
"operation": "update",
|
|
3149
|
+
"app_key": app_key,
|
|
3150
|
+
"record_id": apply_id,
|
|
3151
|
+
"blockers": [blocker],
|
|
3152
|
+
"request_route": self._request_route_payload(context),
|
|
3153
|
+
"view_probe_summary": [
|
|
3154
|
+
self._record_context_probe_summary_payload(probe)
|
|
3155
|
+
for probe in probes
|
|
3156
|
+
],
|
|
3157
|
+
"recommended_next_actions": recommended_next_actions,
|
|
3158
|
+
"fix_hint": (
|
|
3159
|
+
"Confirm the target record still exists and remains visible in at least one accessible view before retrying the update."
|
|
3160
|
+
),
|
|
3161
|
+
},
|
|
3162
|
+
)
|
|
3163
|
+
)
|
|
3164
|
+
|
|
2841
3165
|
def _record_matches_accessible_view(
|
|
2842
3166
|
self,
|
|
2843
3167
|
context, # type: ignore[no-untyped-def]
|
|
@@ -2861,22 +3185,26 @@ class RecordTools(ToolBase):
|
|
|
2861
3185
|
*,
|
|
2862
3186
|
profile: str = DEFAULT_PROFILE,
|
|
2863
3187
|
app_key: str,
|
|
2864
|
-
record_id:
|
|
2865
|
-
record_ids: list[
|
|
3188
|
+
record_id: Any | None = None,
|
|
3189
|
+
record_ids: list[Any] | None = None,
|
|
2866
3190
|
output_profile: str = "normal",
|
|
2867
3191
|
) -> JSONObject:
|
|
2868
3192
|
"""执行记录相关逻辑。"""
|
|
2869
3193
|
normalized_output_profile = self._normalize_public_output_profile(output_profile)
|
|
2870
3194
|
if not app_key:
|
|
2871
3195
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
2872
|
-
normalized_record_ids
|
|
2873
|
-
|
|
3196
|
+
normalized_record_ids: list[int] = []
|
|
3197
|
+
for index, item in enumerate(record_ids or []):
|
|
3198
|
+
normalized_record_ids.append(normalize_positive_id_int(item, field_name=f"record_ids[{index}]"))
|
|
3199
|
+
delete_ids = normalized_record_ids
|
|
3200
|
+
if not delete_ids and record_id is not None:
|
|
3201
|
+
delete_ids = [normalize_positive_id_int(record_id, field_name="record_id")]
|
|
2874
3202
|
if not delete_ids:
|
|
2875
3203
|
raise_tool_error(QingflowApiError.config_error("record_id or record_ids is required"))
|
|
2876
3204
|
normalized_payload = {
|
|
2877
3205
|
"operation": "delete",
|
|
2878
|
-
"record_id": record_id,
|
|
2879
|
-
"record_ids": delete_ids,
|
|
3206
|
+
"record_id": stringify_backend_id(record_id) if record_id is not None else None,
|
|
3207
|
+
"record_ids": [stringify_backend_id(item) for item in delete_ids],
|
|
2880
3208
|
"answers": [],
|
|
2881
3209
|
"submit_type": 1,
|
|
2882
3210
|
}
|
|
@@ -5406,7 +5734,19 @@ class RecordTools(ToolBase):
|
|
|
5406
5734
|
existing_answers_loaded = True
|
|
5407
5735
|
else:
|
|
5408
5736
|
try:
|
|
5409
|
-
|
|
5737
|
+
if resolved_view is not None:
|
|
5738
|
+
existing_answers_for_update, _used_list_type = self._load_record_answers_for_accessible_route(
|
|
5739
|
+
context,
|
|
5740
|
+
app_key=app_key,
|
|
5741
|
+
apply_id=apply_id,
|
|
5742
|
+
resolved_view=resolved_view,
|
|
5743
|
+
)
|
|
5744
|
+
else:
|
|
5745
|
+
existing_answers_for_update = self._load_record_answers_for_preflight(
|
|
5746
|
+
context,
|
|
5747
|
+
app_key=app_key,
|
|
5748
|
+
apply_id=apply_id,
|
|
5749
|
+
)
|
|
5410
5750
|
existing_answers_loaded = True
|
|
5411
5751
|
except QingflowApiError:
|
|
5412
5752
|
validation_warnings.append(
|
|
@@ -6118,12 +6458,22 @@ class RecordTools(ToolBase):
|
|
|
6118
6458
|
force_refresh_form=force_refresh_form,
|
|
6119
6459
|
)
|
|
6120
6460
|
self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6461
|
+
try:
|
|
6462
|
+
result = self.backend.request(
|
|
6463
|
+
"POST",
|
|
6464
|
+
context,
|
|
6465
|
+
f"/app/{app_key}/apply/{normalized_apply_id}",
|
|
6466
|
+
json_body={"role": role, "answers": normalized_answers},
|
|
6467
|
+
)
|
|
6468
|
+
except QingflowApiError as exc:
|
|
6469
|
+
self._remap_record_update_target_context_error(
|
|
6470
|
+
profile,
|
|
6471
|
+
context,
|
|
6472
|
+
app_key=app_key,
|
|
6473
|
+
apply_id=normalized_apply_id,
|
|
6474
|
+
exc=exc,
|
|
6475
|
+
)
|
|
6476
|
+
raise
|
|
6127
6477
|
verification = self._verify_record_write_result(
|
|
6128
6478
|
context,
|
|
6129
6479
|
app_key=app_key,
|
|
@@ -6653,6 +7003,8 @@ class RecordTools(ToolBase):
|
|
|
6653
7003
|
isinstance(payload.get("viewgraphLimit"), list)
|
|
6654
7004
|
or isinstance(payload.get("viewConfig"), dict)
|
|
6655
7005
|
or isinstance(payload.get("viewgraphConfig"), dict)
|
|
7006
|
+
or isinstance(payload.get("viewgraphQuestions"), list)
|
|
7007
|
+
or isinstance(payload.get("viewgraphQueIds"), list)
|
|
6656
7008
|
):
|
|
6657
7009
|
config = payload
|
|
6658
7010
|
else:
|
|
@@ -7144,13 +7496,6 @@ class RecordTools(ToolBase):
|
|
|
7144
7496
|
raise_tool_error(QingflowApiError.config_error("view_id is required; call app_get first to inspect accessible_views"))
|
|
7145
7497
|
|
|
7146
7498
|
system_all_list_type = SYSTEM_VIEW_ID_TO_LIST_TYPE["system:all"]
|
|
7147
|
-
if not self._probe_list_type_access(context, app_key=app_key, list_type=system_all_list_type):
|
|
7148
|
-
raise_tool_error(
|
|
7149
|
-
QingflowApiError.config_error(
|
|
7150
|
-
"view_id is required because system:all is not accessible; call app_get first to inspect accessible_views"
|
|
7151
|
-
)
|
|
7152
|
-
)
|
|
7153
|
-
|
|
7154
7499
|
return (
|
|
7155
7500
|
AccessibleViewRoute(
|
|
7156
7501
|
view_id="system:all",
|
|
@@ -8314,8 +8659,8 @@ class RecordTools(ToolBase):
|
|
|
8314
8659
|
"""执行内部辅助逻辑。"""
|
|
8315
8660
|
payload: JSONObject = {
|
|
8316
8661
|
"operation": operation,
|
|
8317
|
-
"record_id": record_id,
|
|
8318
|
-
"record_ids": record_ids,
|
|
8662
|
+
"record_id": stringify_backend_id(record_id) if record_id is not None else None,
|
|
8663
|
+
"record_ids": [stringify_backend_id(item) for item in record_ids],
|
|
8319
8664
|
"answers": normalized_answers,
|
|
8320
8665
|
"submit_type": submit_type,
|
|
8321
8666
|
}
|
|
@@ -8514,7 +8859,7 @@ class RecordTools(ToolBase):
|
|
|
8514
8859
|
"output_profile": output_profile,
|
|
8515
8860
|
"data": {
|
|
8516
8861
|
"action": {"operation": operation, "executed": True},
|
|
8517
|
-
"resource": raw_apply.get("resource"),
|
|
8862
|
+
"resource": _public_record_resource(raw_apply.get("resource")),
|
|
8518
8863
|
"verification": raw_apply.get("verification"),
|
|
8519
8864
|
"normalized_payload": normalized_payload,
|
|
8520
8865
|
"blockers": [],
|
|
@@ -8564,7 +8909,13 @@ class RecordTools(ToolBase):
|
|
|
8564
8909
|
except json.JSONDecodeError:
|
|
8565
8910
|
parsed = None
|
|
8566
8911
|
if isinstance(parsed, dict):
|
|
8567
|
-
|
|
8912
|
+
parsed_details = parsed.get("details")
|
|
8913
|
+
details_payload = cast(JSONObject, parsed_details) if isinstance(parsed_details, dict) else None
|
|
8914
|
+
error_payload["error_code"] = (
|
|
8915
|
+
parsed.get("error_code")
|
|
8916
|
+
or (details_payload.get("error_code") if details_payload is not None else None)
|
|
8917
|
+
or error_payload["error_code"]
|
|
8918
|
+
)
|
|
8568
8919
|
error_payload["message"] = parsed.get("message") or error_payload["message"]
|
|
8569
8920
|
if parsed.get("backend_code") is not None:
|
|
8570
8921
|
error_payload["backend_code"] = parsed.get("backend_code")
|
|
@@ -8572,6 +8923,8 @@ class RecordTools(ToolBase):
|
|
|
8572
8923
|
error_payload["request_id"] = parsed.get("request_id")
|
|
8573
8924
|
if isinstance(parsed.get("request_route"), dict):
|
|
8574
8925
|
request_route = cast(JSONObject, parsed.get("request_route"))
|
|
8926
|
+
elif details_payload is not None and isinstance(details_payload.get("request_route"), dict):
|
|
8927
|
+
request_route = cast(JSONObject, details_payload.get("request_route"))
|
|
8575
8928
|
response: JSONObject = {
|
|
8576
8929
|
"profile": profile,
|
|
8577
8930
|
"ws_id": None,
|
|
@@ -9859,10 +10212,20 @@ class RecordTools(ToolBase):
|
|
|
9859
10212
|
"""执行内部辅助逻辑。"""
|
|
9860
10213
|
if not app_key:
|
|
9861
10214
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
10215
|
+
return self._normalize_internal_backend_id(apply_id, field_name="apply_id")
|
|
10216
|
+
|
|
10217
|
+
def _normalize_internal_backend_id(self, value: Any, *, field_name: str) -> int:
|
|
10218
|
+
"""Normalize backend/apply ids after the public boundary has already preserved long string ids."""
|
|
10219
|
+
if value in (None, "") or isinstance(value, bool):
|
|
10220
|
+
raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))
|
|
10221
|
+
if isinstance(value, int):
|
|
10222
|
+
if value <= 0:
|
|
10223
|
+
raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))
|
|
10224
|
+
return value
|
|
10225
|
+
text = stringify_backend_id(value)
|
|
10226
|
+
if text is None or not text.isdecimal() or int(text) <= 0:
|
|
10227
|
+
raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))
|
|
10228
|
+
return int(text)
|
|
9866
10229
|
|
|
9867
10230
|
def _validate_record_write(self, app_key: str, answers: list[JSONObject], apply_id: int | None = None) -> None:
|
|
9868
10231
|
"""执行内部辅助逻辑。"""
|
|
@@ -10871,10 +11234,14 @@ def _build_flat_row(answer_list: list[JSONValue], fields: list[FormField], *, ap
|
|
|
10871
11234
|
return row
|
|
10872
11235
|
|
|
10873
11236
|
|
|
10874
|
-
def _public_record_id_text(record_id:
|
|
10875
|
-
if record_id is None or record_id
|
|
11237
|
+
def _public_record_id_text(record_id: Any) -> str | None:
|
|
11238
|
+
if record_id is None or isinstance(record_id, bool):
|
|
11239
|
+
return None
|
|
11240
|
+
if isinstance(record_id, int) and record_id <= 0:
|
|
10876
11241
|
return None
|
|
10877
|
-
|
|
11242
|
+
if isinstance(record_id, str) and (not record_id.strip() or not record_id.strip().isdecimal()):
|
|
11243
|
+
return None
|
|
11244
|
+
return stringify_backend_id(record_id)
|
|
10878
11245
|
|
|
10879
11246
|
|
|
10880
11247
|
def _normalize_public_record_rows(rows: list[JSONValue]) -> list[JSONObject]:
|
|
@@ -11427,10 +11794,29 @@ def _field_mapping_entry(role: str, field: FormField | None, *, requested: str)
|
|
|
11427
11794
|
}
|
|
11428
11795
|
|
|
11429
11796
|
|
|
11430
|
-
def _record_resource_payload(record_id:
|
|
11431
|
-
|
|
11797
|
+
def _record_resource_payload(record_id: Any) -> JSONObject | None:
|
|
11798
|
+
public_record_id = _public_record_id_text(record_id)
|
|
11799
|
+
if public_record_id is None:
|
|
11432
11800
|
return None
|
|
11433
|
-
return {"type": "record", "record_id":
|
|
11801
|
+
return {"type": "record", "record_id": public_record_id, "apply_id": public_record_id}
|
|
11802
|
+
|
|
11803
|
+
|
|
11804
|
+
def _public_record_resource(resource: Any) -> Any:
|
|
11805
|
+
if not isinstance(resource, dict) or resource.get("type") != "record":
|
|
11806
|
+
return resource
|
|
11807
|
+
payload = dict(resource)
|
|
11808
|
+
if "record_id" in payload:
|
|
11809
|
+
payload["record_id"] = _public_record_id_text(payload.get("record_id"))
|
|
11810
|
+
if "apply_id" in payload:
|
|
11811
|
+
payload["apply_id"] = _public_record_id_text(payload.get("apply_id"))
|
|
11812
|
+
record_ids = payload.get("record_ids")
|
|
11813
|
+
if isinstance(record_ids, list):
|
|
11814
|
+
payload["record_ids"] = [
|
|
11815
|
+
stringify_backend_id(item)
|
|
11816
|
+
for item in record_ids
|
|
11817
|
+
if stringify_backend_id(item) is not None
|
|
11818
|
+
]
|
|
11819
|
+
return payload
|
|
11434
11820
|
|
|
11435
11821
|
|
|
11436
11822
|
def _query_id() -> str:
|