@qingflow-tech/qingflow-app-builder-mcp 1.0.10 → 1.0.12
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 +6 -3
- package/docs/local-agent-install.md +54 -3
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/lib/runtime.mjs +304 -13
- package/npm/scripts/postinstall.mjs +1 -5
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +12 -12
- package/skills/qingflow-app-builder/references/create-app.md +3 -3
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +1 -1
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +6 -5
- package/skills/qingflow-app-builder/references/update-views.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -3
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +41 -2
- package/src/qingflow_mcp/builder_facade/service.py +2743 -423
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +30 -4
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +1 -1
- package/src/qingflow_mcp/cli/commands/record.py +54 -11
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +238 -8
- package/src/qingflow_mcp/cli/main.py +47 -3
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +24 -16
- package/src/qingflow_mcp/response_trim.py +119 -12
- package/src/qingflow_mcp/server.py +17 -14
- package/src/qingflow_mcp/server_app_builder.py +29 -7
- package/src/qingflow_mcp/server_app_user.py +23 -24
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
- package/src/qingflow_mcp/solution/executor.py +112 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +497 -65
- package/src/qingflow_mcp/tools/app_tools.py +237 -51
- package/src/qingflow_mcp/tools/approval_tools.py +196 -34
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +230 -33
- package/src/qingflow_mcp/tools/file_tools.py +7 -3
- package/src/qingflow_mcp/tools/import_tools.py +293 -40
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +134 -8
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +2305 -442
- package/src/qingflow_mcp/tools/resource_read_tools.py +191 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +57 -15
- package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
|
@@ -14,7 +14,7 @@ from datetime import UTC, datetime, timedelta
|
|
|
14
14
|
from decimal import Decimal, InvalidOperation
|
|
15
15
|
from io import BytesIO
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import Any, cast
|
|
17
|
+
from typing import Any, Callable, cast
|
|
18
18
|
from urllib.parse import parse_qs, unquote, urlsplit
|
|
19
19
|
from uuid import uuid4
|
|
20
20
|
from xml.etree import ElementTree
|
|
@@ -22,7 +22,7 @@ from xml.etree import ElementTree
|
|
|
22
22
|
from mcp.server.fastmcp import FastMCP
|
|
23
23
|
|
|
24
24
|
from ..config import DEFAULT_PROFILE, DEFAULT_RECORD_LIST_TYPE, DEFAULT_USER_AGENT, get_mcp_home
|
|
25
|
-
from ..errors import QingflowApiError, raise_tool_error
|
|
25
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
|
|
26
26
|
from ..id_utils import normalize_positive_id_int, stringify_backend_id
|
|
27
27
|
from ..json_types import JSONObject, JSONScalar, JSONValue
|
|
28
28
|
from ..list_type_labels import (
|
|
@@ -45,6 +45,11 @@ RECORD_ACCESS_UNBOUNDED_ROW_THRESHOLD = 50_000
|
|
|
45
45
|
RECORD_ACCESS_TIME_BUDGET_SECONDS = 55.0
|
|
46
46
|
RECORD_ACCESS_MIN_REMAINING_SECONDS = 8.0
|
|
47
47
|
RECORD_GET_DETAIL_LOG_PAGE_SIZE = 10
|
|
48
|
+
RECORD_LOGS_PAGE_SIZE = 200
|
|
49
|
+
RECORD_LOGS_PREVIEW_LIMIT = 10
|
|
50
|
+
RECORD_LOGS_MAX_ITEMS = 20_000
|
|
51
|
+
RECORD_LOGS_TIME_BUDGET_SECONDS = 55.0
|
|
52
|
+
RECORD_LOGS_MIN_REMAINING_SECONDS = 8.0
|
|
48
53
|
RECORD_GET_MEDIA_MAX_IMAGES = 30
|
|
49
54
|
RECORD_GET_MEDIA_MAX_IMAGE_BYTES = 20 * 1024 * 1024
|
|
50
55
|
RECORD_GET_MEDIA_MAX_TOTAL_BYTES = 100 * 1024 * 1024
|
|
@@ -126,6 +131,27 @@ SCHEMA_LINKAGE_REFERENCE_SOURCE_MESSAGE = "updating this field may auto-fill or
|
|
|
126
131
|
SCHEMA_LINKAGE_REFERENCE_TARGET_MESSAGE = "this field is usually filled from an upstream reference selection or default matching logic"
|
|
127
132
|
SCHEMA_LINKAGE_REFERENCE_BOTH_MESSAGE = "this field participates in reference-driven auto-fill logic"
|
|
128
133
|
SCHEMA_LINKAGE_FORMULA_MESSAGE = "this field is usually derived by formula or default auto-fill logic"
|
|
134
|
+
OPTIONAL_SCHEMA_PERMISSION_CODES = {40002, 40027, 404}
|
|
135
|
+
RECORD_PERMISSION_DENIED_CODES = {40002, 40027}
|
|
136
|
+
SYSTEM_VIEW_LIST_TYPES = {int(list_type) for _view_id, list_type, _name in SYSTEM_VIEW_DEFINITIONS}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _is_optional_schema_permission_error(error: QingflowApiError) -> bool:
|
|
140
|
+
if is_auth_like_error(error):
|
|
141
|
+
return False
|
|
142
|
+
return backend_code_int(error) in OPTIONAL_SCHEMA_PERMISSION_CODES or error.http_status == 404
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _is_record_permission_denied_error(error: QingflowApiError) -> bool:
|
|
146
|
+
if is_auth_like_error(error):
|
|
147
|
+
return False
|
|
148
|
+
return backend_code_int(error) in RECORD_PERMISSION_DENIED_CODES
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _is_optional_record_auxiliary_lookup_error(error: QingflowApiError) -> bool:
|
|
152
|
+
if is_auth_like_error(error):
|
|
153
|
+
return False
|
|
154
|
+
return backend_code_int(error) in {40002, 40027, 404} or error.http_status == 404
|
|
129
155
|
|
|
130
156
|
|
|
131
157
|
@dataclass(slots=True)
|
|
@@ -210,6 +236,13 @@ class AccessibleViewRoute:
|
|
|
210
236
|
view_type: str | None = None
|
|
211
237
|
|
|
212
238
|
|
|
239
|
+
def _prefer_custom_update_routes(routes: list[AccessibleViewRoute]) -> list[AccessibleViewRoute]:
|
|
240
|
+
return [
|
|
241
|
+
*[route for route in routes if route.kind == "custom"],
|
|
242
|
+
*[route for route in routes if route.kind != "custom"],
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
|
|
213
246
|
@dataclass(slots=True)
|
|
214
247
|
class RecordContextRouteProbe:
|
|
215
248
|
route: AccessibleViewRoute
|
|
@@ -269,6 +302,30 @@ GENERIC_FIELD_ALIAS_OVERRIDES: dict[str, list[str]] = {
|
|
|
269
302
|
FIELD_LOOKUP_STRIP_RE = re.compile(r"[\s_()()\[\]【】{}<>·/\\::-]+")
|
|
270
303
|
|
|
271
304
|
|
|
305
|
+
def _pick_route_payload(payload: JSONObject) -> JSONObject:
|
|
306
|
+
return {
|
|
307
|
+
key: payload[key]
|
|
308
|
+
for key in (
|
|
309
|
+
"route_type",
|
|
310
|
+
"endpoint_kind",
|
|
311
|
+
"status",
|
|
312
|
+
"role",
|
|
313
|
+
"task_id",
|
|
314
|
+
"workflow_node_id",
|
|
315
|
+
"view_id",
|
|
316
|
+
"view_key",
|
|
317
|
+
"view_name",
|
|
318
|
+
"error_code",
|
|
319
|
+
"backend_code",
|
|
320
|
+
"http_status",
|
|
321
|
+
"request_id",
|
|
322
|
+
"message",
|
|
323
|
+
"reason",
|
|
324
|
+
)
|
|
325
|
+
if key in payload and payload[key] not in (None, "", [], {})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
272
329
|
class RecordTools(ToolBase):
|
|
273
330
|
"""记录工具(中文名:记录读写与分析)。
|
|
274
331
|
|
|
@@ -442,6 +499,20 @@ class RecordTools(ToolBase):
|
|
|
442
499
|
output_profile=output_profile,
|
|
443
500
|
)
|
|
444
501
|
|
|
502
|
+
@mcp.tool(description="Read all visible data logs and workflow logs for one Qingflow record into local JSONL files. Requires the same view_id as the frontend record context. This tool hides pagination and returns file paths plus completeness metadata.")
|
|
503
|
+
def record_logs_get(
|
|
504
|
+
profile: str = DEFAULT_PROFILE,
|
|
505
|
+
app_key: str = "",
|
|
506
|
+
record_id: str = "",
|
|
507
|
+
view_id: str | None = None,
|
|
508
|
+
) -> JSONObject:
|
|
509
|
+
return self.record_logs_get(
|
|
510
|
+
profile=profile,
|
|
511
|
+
app_key=app_key,
|
|
512
|
+
record_id=record_id,
|
|
513
|
+
view_id=view_id,
|
|
514
|
+
)
|
|
515
|
+
|
|
445
516
|
@mcp.tool()
|
|
446
517
|
def record_browse_schema_get(
|
|
447
518
|
app_key: str = "",
|
|
@@ -459,12 +530,14 @@ class RecordTools(ToolBase):
|
|
|
459
530
|
def record_update_schema_get(
|
|
460
531
|
app_key: str = "",
|
|
461
532
|
record_id: str = "",
|
|
533
|
+
view_id: str | None = None,
|
|
462
534
|
output_profile: str = "normal",
|
|
463
535
|
) -> JSONObject:
|
|
464
536
|
return self.record_update_schema_get_public(
|
|
465
537
|
profile=DEFAULT_PROFILE,
|
|
466
538
|
app_key=app_key,
|
|
467
539
|
record_id=record_id,
|
|
540
|
+
view_id=view_id,
|
|
468
541
|
output_profile=output_profile,
|
|
469
542
|
)
|
|
470
543
|
|
|
@@ -493,8 +566,10 @@ class RecordTools(ToolBase):
|
|
|
493
566
|
@mcp.tool(
|
|
494
567
|
description=(
|
|
495
568
|
"Update one Qingflow record using a field map. "
|
|
496
|
-
"
|
|
497
|
-
"
|
|
569
|
+
"For simple field changes, call this tool directly after the target record is clear. "
|
|
570
|
+
"Pass view_id when the frontend detail view is known; the tool will try that view first. "
|
|
571
|
+
"It first tries the data-manager direct route, then falls back to the frontend custom-view edit route when available. "
|
|
572
|
+
"Use record_update_schema_get for diagnostics or complex field-scope inspection."
|
|
498
573
|
)
|
|
499
574
|
)
|
|
500
575
|
def record_update(
|
|
@@ -502,6 +577,7 @@ class RecordTools(ToolBase):
|
|
|
502
577
|
record_id: str | None = None,
|
|
503
578
|
fields: JSONObject | None = None,
|
|
504
579
|
items: list[JSONObject] | None = None,
|
|
580
|
+
view_id: str | None = None,
|
|
505
581
|
dry_run: bool = False,
|
|
506
582
|
verify_write: bool = True,
|
|
507
583
|
output_profile: str = "normal",
|
|
@@ -512,6 +588,7 @@ class RecordTools(ToolBase):
|
|
|
512
588
|
record_id=record_id,
|
|
513
589
|
fields=fields,
|
|
514
590
|
items=items,
|
|
591
|
+
view_id=view_id,
|
|
515
592
|
dry_run=dry_run,
|
|
516
593
|
verify_write=verify_write,
|
|
517
594
|
output_profile=output_profile,
|
|
@@ -520,13 +597,14 @@ class RecordTools(ToolBase):
|
|
|
520
597
|
@mcp.tool(
|
|
521
598
|
description=(
|
|
522
599
|
"Delete Qingflow records by record_id or record_ids. "
|
|
523
|
-
"
|
|
600
|
+
"Pass view_id when deleting from a known system list route; custom view deletion is not supported by this backend route."
|
|
524
601
|
)
|
|
525
602
|
)
|
|
526
603
|
def record_delete(
|
|
527
604
|
app_key: str = "",
|
|
528
605
|
record_id: str | None = None,
|
|
529
606
|
record_ids: list[str] | None = None,
|
|
607
|
+
view_id: str | None = None,
|
|
530
608
|
output_profile: str = "normal",
|
|
531
609
|
) -> JSONObject:
|
|
532
610
|
return self.record_delete_public(
|
|
@@ -534,6 +612,7 @@ class RecordTools(ToolBase):
|
|
|
534
612
|
app_key=app_key,
|
|
535
613
|
record_id=record_id,
|
|
536
614
|
record_ids=record_ids or [],
|
|
615
|
+
view_id=view_id,
|
|
537
616
|
output_profile=output_profile,
|
|
538
617
|
)
|
|
539
618
|
|
|
@@ -805,6 +884,7 @@ class RecordTools(ToolBase):
|
|
|
805
884
|
profile: str = DEFAULT_PROFILE,
|
|
806
885
|
app_key: str,
|
|
807
886
|
record_id: Any,
|
|
887
|
+
view_id: str | None = None,
|
|
808
888
|
output_profile: str = "normal",
|
|
809
889
|
) -> JSONObject:
|
|
810
890
|
"""执行记录相关逻辑。"""
|
|
@@ -816,21 +896,44 @@ class RecordTools(ToolBase):
|
|
|
816
896
|
def runner(session_profile, context):
|
|
817
897
|
request_route = self._request_route_payload(context)
|
|
818
898
|
self._clear_record_schema_caches(profile=profile, app_key=app_key, clear_view_caches=True)
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
899
|
+
linkage_payloads_by_field_id: dict[str, JSONObject] = {}
|
|
900
|
+
try:
|
|
901
|
+
app_schema = self._get_form_schema(profile, context, app_key, force_refresh=True)
|
|
902
|
+
except QingflowApiError as exc:
|
|
903
|
+
if not _is_optional_schema_permission_error(exc):
|
|
904
|
+
raise
|
|
905
|
+
else:
|
|
906
|
+
question_relations = _collect_question_relations(app_schema)
|
|
907
|
+
app_index = _build_applicant_top_level_field_index(app_schema)
|
|
908
|
+
linked_field_ids = _collect_linked_required_field_ids(question_relations)
|
|
909
|
+
linked_field_ids.update(_collect_option_linked_field_ids(app_index))
|
|
910
|
+
linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
|
|
911
|
+
app_schema,
|
|
912
|
+
linked_field_ids=linked_field_ids,
|
|
913
|
+
)
|
|
914
|
+
app_index = _merge_field_indexes(app_index, linked_hidden_index)
|
|
915
|
+
linkage_payloads_by_field_id = _build_static_schema_linkage_payloads(
|
|
916
|
+
index=app_index,
|
|
917
|
+
question_relations=question_relations,
|
|
918
|
+
)
|
|
919
|
+
preferred_view_id = _normalize_optional_text(view_id)
|
|
833
920
|
candidate_routes = self._candidate_update_views(profile, context, app_key)
|
|
921
|
+
if preferred_view_id:
|
|
922
|
+
preferred_route = next(
|
|
923
|
+
(
|
|
924
|
+
route
|
|
925
|
+
for route in candidate_routes
|
|
926
|
+
if route.view_id == preferred_view_id
|
|
927
|
+
),
|
|
928
|
+
None,
|
|
929
|
+
)
|
|
930
|
+
if preferred_route is None:
|
|
931
|
+
raise_tool_error(
|
|
932
|
+
QingflowApiError.config_error(
|
|
933
|
+
f"view_id '{preferred_view_id}' is not an accessible update candidate"
|
|
934
|
+
)
|
|
935
|
+
)
|
|
936
|
+
candidate_routes = [preferred_route]
|
|
834
937
|
probes = self._probe_candidate_record_contexts(
|
|
835
938
|
context,
|
|
836
939
|
app_key=app_key,
|
|
@@ -932,6 +1035,7 @@ class RecordTools(ToolBase):
|
|
|
932
1035
|
output_profile=normalized_output_profile,
|
|
933
1036
|
view_probe_summary=probe_summary,
|
|
934
1037
|
ambiguous_fields=[],
|
|
1038
|
+
preferred_view_id=preferred_view_id,
|
|
935
1039
|
)
|
|
936
1040
|
|
|
937
1041
|
ambiguous_field_ids: set[int] = set()
|
|
@@ -978,6 +1082,7 @@ class RecordTools(ToolBase):
|
|
|
978
1082
|
output_profile=normalized_output_profile,
|
|
979
1083
|
view_probe_summary=probe_summary,
|
|
980
1084
|
ambiguous_fields=ambiguous_fields,
|
|
1085
|
+
preferred_view_id=preferred_view_id,
|
|
981
1086
|
)
|
|
982
1087
|
|
|
983
1088
|
response: JSONObject = {
|
|
@@ -995,15 +1100,61 @@ class RecordTools(ToolBase):
|
|
|
995
1100
|
item["title"]: self._ready_schema_template_value(item)
|
|
996
1101
|
for item in writable_fields
|
|
997
1102
|
},
|
|
1103
|
+
"available_update_routes": self._record_update_schema_available_routes(matched_probes),
|
|
1104
|
+
"recommended_update_route": {
|
|
1105
|
+
"route_type": "auto",
|
|
1106
|
+
"order": ["admin_direct", "view_edit", "task_save_only"],
|
|
1107
|
+
"message": "record_update will try data-manager direct edit first, then a matching custom-view edit route, then a unique current-user todo save-only route when the target fields are editable on that workflow node.",
|
|
1108
|
+
},
|
|
998
1109
|
}
|
|
1110
|
+
if preferred_view_id:
|
|
1111
|
+
response["preferred_view_id"] = preferred_view_id
|
|
999
1112
|
if normalized_output_profile == "verbose":
|
|
1000
1113
|
response["view_probe_summary"] = probe_summary
|
|
1001
1114
|
response["record_context_probe"] = probe_summary
|
|
1002
1115
|
response["ambiguous_fields"] = ambiguous_fields
|
|
1116
|
+
response["route_probe_summary"] = probe_summary
|
|
1003
1117
|
return response
|
|
1004
1118
|
|
|
1005
1119
|
return self._run_record_tool(profile, runner)
|
|
1006
1120
|
|
|
1121
|
+
def _record_update_schema_available_routes(self, matched_probes: list[RecordContextRouteProbe]) -> list[JSONObject]:
|
|
1122
|
+
routes: list[JSONObject] = [
|
|
1123
|
+
{
|
|
1124
|
+
"route_type": "admin_direct",
|
|
1125
|
+
"endpoint_kind": "app_apply_update",
|
|
1126
|
+
"role": 1,
|
|
1127
|
+
"availability": "attempted_on_update",
|
|
1128
|
+
"message": "Requires data-manager edit permission; record_update safely falls back if backend returns permission denied.",
|
|
1129
|
+
}
|
|
1130
|
+
]
|
|
1131
|
+
for probe in matched_probes:
|
|
1132
|
+
if probe.route.kind != "custom":
|
|
1133
|
+
continue
|
|
1134
|
+
view_key = self._route_view_key(probe.route)
|
|
1135
|
+
if not view_key:
|
|
1136
|
+
continue
|
|
1137
|
+
routes.append(
|
|
1138
|
+
{
|
|
1139
|
+
"route_type": "view_edit",
|
|
1140
|
+
"endpoint_kind": "view_apply_update",
|
|
1141
|
+
"view_id": probe.route.view_id,
|
|
1142
|
+
"view_key": view_key,
|
|
1143
|
+
"view_name": probe.route.name,
|
|
1144
|
+
"availability": "candidate",
|
|
1145
|
+
"message": "Uses the same custom-view detail edit route as the frontend.",
|
|
1146
|
+
}
|
|
1147
|
+
)
|
|
1148
|
+
routes.append(
|
|
1149
|
+
{
|
|
1150
|
+
"route_type": "task_save_only",
|
|
1151
|
+
"endpoint_kind": "workflow_node_save_only",
|
|
1152
|
+
"availability": "auto_probe_on_update",
|
|
1153
|
+
"message": "record_update probes the current user's todo list and uses save-only only when exactly one matching task exists and the requested fields are editable on that workflow node.",
|
|
1154
|
+
}
|
|
1155
|
+
)
|
|
1156
|
+
return routes
|
|
1157
|
+
|
|
1007
1158
|
def _record_update_schema_blocked_response(
|
|
1008
1159
|
self,
|
|
1009
1160
|
*,
|
|
@@ -1018,6 +1169,7 @@ class RecordTools(ToolBase):
|
|
|
1018
1169
|
output_profile: str,
|
|
1019
1170
|
view_probe_summary: list[JSONObject],
|
|
1020
1171
|
ambiguous_fields: list[JSONObject],
|
|
1172
|
+
preferred_view_id: str | None = None,
|
|
1021
1173
|
) -> JSONObject:
|
|
1022
1174
|
"""执行内部辅助逻辑。"""
|
|
1023
1175
|
response: JSONObject = {
|
|
@@ -1035,6 +1187,8 @@ class RecordTools(ToolBase):
|
|
|
1035
1187
|
"payload_template": {},
|
|
1036
1188
|
"recommended_next_actions": recommended_next_actions,
|
|
1037
1189
|
}
|
|
1190
|
+
if preferred_view_id:
|
|
1191
|
+
response["preferred_view_id"] = preferred_view_id
|
|
1038
1192
|
if output_profile == "verbose":
|
|
1039
1193
|
response["view_probe_summary"] = view_probe_summary
|
|
1040
1194
|
response["ambiguous_fields"] = ambiguous_fields
|
|
@@ -1352,24 +1506,58 @@ class RecordTools(ToolBase):
|
|
|
1352
1506
|
)
|
|
1353
1507
|
warnings: list[JSONObject] = []
|
|
1354
1508
|
scope_source = "static_applicant_scope"
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1509
|
+
try:
|
|
1510
|
+
if runtime_lookup:
|
|
1511
|
+
state = self._build_candidate_lookup_state(
|
|
1512
|
+
profile,
|
|
1513
|
+
context,
|
|
1514
|
+
app_key=app_key,
|
|
1515
|
+
record_id=record_id_int,
|
|
1516
|
+
workflow_node_id=workflow_node_id,
|
|
1517
|
+
fields=normalized_fields,
|
|
1518
|
+
)
|
|
1519
|
+
items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1520
|
+
scope_source = "backend_runtime_scope"
|
|
1521
|
+
else:
|
|
1522
|
+
items: list[JSONObject] | None = None
|
|
1523
|
+
if self._member_candidate_static_preview_should_use_backend(field):
|
|
1524
|
+
state = self._build_candidate_lookup_state(
|
|
1525
|
+
profile,
|
|
1526
|
+
context,
|
|
1527
|
+
app_key=app_key,
|
|
1528
|
+
record_id=None,
|
|
1529
|
+
workflow_node_id=None,
|
|
1530
|
+
fields={},
|
|
1531
|
+
)
|
|
1532
|
+
items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1533
|
+
scope_source = "backend_applicant_scope"
|
|
1534
|
+
if items is None:
|
|
1535
|
+
items = self._resolve_member_candidates(context, field, keyword=keyword)
|
|
1536
|
+
warnings.append(
|
|
1537
|
+
{
|
|
1538
|
+
"code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
|
|
1539
|
+
"message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
|
|
1540
|
+
}
|
|
1541
|
+
)
|
|
1542
|
+
except (RecordInputError, QingflowApiError) as error:
|
|
1543
|
+
record_error = (
|
|
1544
|
+
error
|
|
1545
|
+
if isinstance(error, RecordInputError)
|
|
1546
|
+
else self._candidate_lookup_error(kind="member", field=field, value=keyword, error=error)
|
|
1547
|
+
)
|
|
1548
|
+
return self._candidate_lookup_failed_response(
|
|
1549
|
+
profile=profile,
|
|
1550
|
+
session_profile=session_profile,
|
|
1551
|
+
context=context,
|
|
1552
|
+
kind="member",
|
|
1553
|
+
error=record_error,
|
|
1554
|
+
field=field,
|
|
1359
1555
|
app_key=app_key,
|
|
1360
|
-
|
|
1556
|
+
record_id_text=record_id_text,
|
|
1361
1557
|
workflow_node_id=workflow_node_id,
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
scope_source = "backend_runtime_scope"
|
|
1366
|
-
else:
|
|
1367
|
-
items = self._resolve_member_candidates(context, field, keyword=keyword)
|
|
1368
|
-
warnings.append(
|
|
1369
|
-
{
|
|
1370
|
-
"code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
|
|
1371
|
-
"message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
|
|
1372
|
-
}
|
|
1558
|
+
fields_present=bool(normalized_fields),
|
|
1559
|
+
keyword=keyword,
|
|
1560
|
+
scope_source=scope_source,
|
|
1373
1561
|
)
|
|
1374
1562
|
total = len(items)
|
|
1375
1563
|
start = (page_num - 1) * page_size
|
|
@@ -1462,41 +1650,75 @@ class RecordTools(ToolBase):
|
|
|
1462
1650
|
)
|
|
1463
1651
|
warnings: list[JSONObject] = []
|
|
1464
1652
|
scope_source = "static_applicant_scope"
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
profile,
|
|
1468
|
-
context,
|
|
1469
|
-
app_key=app_key,
|
|
1470
|
-
record_id=record_id_int,
|
|
1471
|
-
workflow_node_id=workflow_node_id,
|
|
1472
|
-
fields=normalized_fields,
|
|
1473
|
-
)
|
|
1474
|
-
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1475
|
-
scope_source = "backend_runtime_scope"
|
|
1476
|
-
else:
|
|
1477
|
-
items = self._resolve_department_candidates(context, field, keyword=keyword)
|
|
1478
|
-
scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
|
|
1479
|
-
if (
|
|
1480
|
-
not items
|
|
1481
|
-
and field.dept_select_scope_type == 2
|
|
1482
|
-
and not _scope_has_dynamic_or_external(scope)
|
|
1483
|
-
and not list(scope.get("depart") or [])
|
|
1484
|
-
):
|
|
1653
|
+
try:
|
|
1654
|
+
if runtime_lookup:
|
|
1485
1655
|
state = self._build_candidate_lookup_state(
|
|
1486
1656
|
profile,
|
|
1487
1657
|
context,
|
|
1488
1658
|
app_key=app_key,
|
|
1489
|
-
record_id=
|
|
1490
|
-
workflow_node_id=
|
|
1491
|
-
fields=
|
|
1659
|
+
record_id=record_id_int,
|
|
1660
|
+
workflow_node_id=workflow_node_id,
|
|
1661
|
+
fields=normalized_fields,
|
|
1492
1662
|
)
|
|
1493
1663
|
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1494
1664
|
scope_source = "backend_runtime_scope"
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1665
|
+
else:
|
|
1666
|
+
items: list[JSONObject] | None = None
|
|
1667
|
+
if self._department_candidate_static_preview_should_use_backend(field):
|
|
1668
|
+
state = self._build_candidate_lookup_state(
|
|
1669
|
+
profile,
|
|
1670
|
+
context,
|
|
1671
|
+
app_key=app_key,
|
|
1672
|
+
record_id=None,
|
|
1673
|
+
workflow_node_id=None,
|
|
1674
|
+
fields={},
|
|
1675
|
+
)
|
|
1676
|
+
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1677
|
+
scope_source = "backend_applicant_scope"
|
|
1678
|
+
if items is None:
|
|
1679
|
+
items = self._resolve_department_candidates(context, field, keyword=keyword)
|
|
1680
|
+
scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
|
|
1681
|
+
if (
|
|
1682
|
+
not items
|
|
1683
|
+
and field.dept_select_scope_type == 2
|
|
1684
|
+
and not _scope_has_dynamic_or_external(scope)
|
|
1685
|
+
and not list(scope.get("depart") or [])
|
|
1686
|
+
):
|
|
1687
|
+
state = self._build_candidate_lookup_state(
|
|
1688
|
+
profile,
|
|
1689
|
+
context,
|
|
1690
|
+
app_key=app_key,
|
|
1691
|
+
record_id=None,
|
|
1692
|
+
workflow_node_id=None,
|
|
1693
|
+
fields={},
|
|
1694
|
+
)
|
|
1695
|
+
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1696
|
+
scope_source = "backend_applicant_scope"
|
|
1697
|
+
warnings.append(
|
|
1698
|
+
{
|
|
1699
|
+
"code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
|
|
1700
|
+
"message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
|
|
1701
|
+
}
|
|
1702
|
+
)
|
|
1703
|
+
except (RecordInputError, QingflowApiError) as error:
|
|
1704
|
+
record_error = (
|
|
1705
|
+
error
|
|
1706
|
+
if isinstance(error, RecordInputError)
|
|
1707
|
+
else self._candidate_lookup_error(kind="department", field=field, value=keyword, error=error)
|
|
1708
|
+
)
|
|
1709
|
+
return self._candidate_lookup_failed_response(
|
|
1710
|
+
profile=profile,
|
|
1711
|
+
session_profile=session_profile,
|
|
1712
|
+
context=context,
|
|
1713
|
+
kind="department",
|
|
1714
|
+
error=record_error,
|
|
1715
|
+
field=field,
|
|
1716
|
+
app_key=app_key,
|
|
1717
|
+
record_id_text=record_id_text,
|
|
1718
|
+
workflow_node_id=workflow_node_id,
|
|
1719
|
+
fields_present=bool(normalized_fields),
|
|
1720
|
+
keyword=keyword,
|
|
1721
|
+
scope_source=scope_source,
|
|
1500
1722
|
)
|
|
1501
1723
|
total = len(items)
|
|
1502
1724
|
start = (page_num - 1) * page_size
|
|
@@ -1558,6 +1780,21 @@ class RecordTools(ToolBase):
|
|
|
1558
1780
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
1559
1781
|
if limit <= 0:
|
|
1560
1782
|
raise_tool_error(QingflowApiError.config_error("limit must be positive"))
|
|
1783
|
+
if not (
|
|
1784
|
+
_normalize_optional_text(view_id)
|
|
1785
|
+
or list_type is not None
|
|
1786
|
+
or _normalize_optional_text(view_key)
|
|
1787
|
+
or _normalize_optional_text(view_name)
|
|
1788
|
+
):
|
|
1789
|
+
raise_tool_error(
|
|
1790
|
+
QingflowApiError.config_error(
|
|
1791
|
+
"record_analyze requires view_id. Call app_get first and pass accessible_views[].view_id.",
|
|
1792
|
+
details={
|
|
1793
|
+
"error_code": "RECORD_ANALYZE_VIEW_REQUIRED",
|
|
1794
|
+
"fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_analyze with view_id.",
|
|
1795
|
+
},
|
|
1796
|
+
)
|
|
1797
|
+
)
|
|
1561
1798
|
legacy_warnings = _detect_analyze_legacy_warnings(
|
|
1562
1799
|
dimensions=dimensions,
|
|
1563
1800
|
metrics=metrics,
|
|
@@ -1574,7 +1811,7 @@ class RecordTools(ToolBase):
|
|
|
1574
1811
|
list_type=list_type,
|
|
1575
1812
|
view_key=view_key,
|
|
1576
1813
|
view_name=view_name,
|
|
1577
|
-
allow_default=
|
|
1814
|
+
allow_default=False,
|
|
1578
1815
|
)
|
|
1579
1816
|
if not _view_type_supports_analysis(resolved_view.view_type):
|
|
1580
1817
|
raise_tool_error(
|
|
@@ -1655,6 +1892,21 @@ class RecordTools(ToolBase):
|
|
|
1655
1892
|
raise_tool_error(QingflowApiError.config_error("limit must be positive"))
|
|
1656
1893
|
if page <= 0:
|
|
1657
1894
|
raise_tool_error(QingflowApiError.config_error("page must be positive"))
|
|
1895
|
+
if not (
|
|
1896
|
+
_normalize_optional_text(view_id)
|
|
1897
|
+
or list_type is not None
|
|
1898
|
+
or _normalize_optional_text(view_key)
|
|
1899
|
+
or _normalize_optional_text(view_name)
|
|
1900
|
+
):
|
|
1901
|
+
raise_tool_error(
|
|
1902
|
+
QingflowApiError.config_error(
|
|
1903
|
+
"record_list requires view_id. Call app_get first and pass accessible_views[].view_id.",
|
|
1904
|
+
details={
|
|
1905
|
+
"error_code": "RECORD_LIST_VIEW_REQUIRED",
|
|
1906
|
+
"fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_list with view_id.",
|
|
1907
|
+
},
|
|
1908
|
+
)
|
|
1909
|
+
)
|
|
1658
1910
|
view_route, compatibility_warnings = self._resolve_accessible_view_route_for_public(
|
|
1659
1911
|
profile=profile,
|
|
1660
1912
|
app_key=app_key,
|
|
@@ -1662,7 +1914,7 @@ class RecordTools(ToolBase):
|
|
|
1662
1914
|
list_type=list_type,
|
|
1663
1915
|
view_key=view_key,
|
|
1664
1916
|
view_name=view_name,
|
|
1665
|
-
allow_default=
|
|
1917
|
+
allow_default=False,
|
|
1666
1918
|
)
|
|
1667
1919
|
if not _view_type_supports_analysis(view_route.view_type):
|
|
1668
1920
|
raise_tool_error(
|
|
@@ -2136,6 +2388,7 @@ class RecordTools(ToolBase):
|
|
|
2136
2388
|
requested_output_profile = _normalize_record_get_detail_output_profile(output_profile)
|
|
2137
2389
|
record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
|
|
2138
2390
|
normalized_columns = _normalize_public_column_selectors(columns)
|
|
2391
|
+
explicit_view_id = _normalize_optional_text(view_id)
|
|
2139
2392
|
|
|
2140
2393
|
def runner(session_profile, context):
|
|
2141
2394
|
resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
|
|
@@ -2161,89 +2414,323 @@ class RecordTools(ToolBase):
|
|
|
2161
2414
|
"code": "OUTPUT_PROFILE_DEPRECATED_FOR_DETAIL_CONTEXT",
|
|
2162
2415
|
"message": f"output_profile={requested_output_profile!r} is deprecated for record_get; detail_context is always returned.",
|
|
2163
2416
|
})
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2417
|
+
def get_detail_for_route(route: AccessibleViewRoute, route_warnings: list[JSONObject]) -> JSONObject:
|
|
2418
|
+
return self._record_get_detail_context(
|
|
2419
|
+
profile=profile,
|
|
2420
|
+
session_profile=session_profile,
|
|
2421
|
+
context=context,
|
|
2422
|
+
app_key=app_key,
|
|
2423
|
+
record_id_int=record_id_int,
|
|
2424
|
+
resolved_view=route,
|
|
2425
|
+
requested_focus_field_ids=normalized_columns,
|
|
2426
|
+
workflow_node_id=workflow_node_id,
|
|
2427
|
+
warnings=route_warnings,
|
|
2428
|
+
)
|
|
2429
|
+
|
|
2430
|
+
try:
|
|
2431
|
+
return get_detail_for_route(resolved_view, warnings)
|
|
2432
|
+
except QingflowApiError as exc:
|
|
2433
|
+
if explicit_view_id is not None:
|
|
2434
|
+
raise
|
|
2435
|
+
if not self._is_record_context_route_miss(exc):
|
|
2436
|
+
raise
|
|
2437
|
+
fallback_warnings = list(warnings)
|
|
2438
|
+
fallback_warnings.append(
|
|
2439
|
+
{
|
|
2440
|
+
"code": "DEFAULT_DETAIL_ROUTE_DENIED",
|
|
2441
|
+
"message": "record_get default system:all route was not readable; trying accessible views that match the frontend route model.",
|
|
2442
|
+
"backend_code": exc.backend_code,
|
|
2443
|
+
}
|
|
2444
|
+
)
|
|
2445
|
+
last_error = exc
|
|
2446
|
+
for candidate in self._candidate_update_views(profile, context, app_key):
|
|
2447
|
+
if candidate.view_id == resolved_view.view_id:
|
|
2448
|
+
continue
|
|
2449
|
+
try:
|
|
2450
|
+
return get_detail_for_route(candidate, fallback_warnings)
|
|
2451
|
+
except QingflowApiError as candidate_exc:
|
|
2452
|
+
if not self._is_record_context_route_miss(candidate_exc):
|
|
2453
|
+
raise
|
|
2454
|
+
last_error = candidate_exc
|
|
2455
|
+
raise last_error
|
|
2175
2456
|
|
|
2176
2457
|
return self._run_record_tool(profile, runner)
|
|
2177
2458
|
|
|
2178
|
-
|
|
2459
|
+
@tool_cn_name("记录全量日志")
|
|
2460
|
+
def record_logs_get(
|
|
2179
2461
|
self,
|
|
2180
2462
|
*,
|
|
2181
2463
|
profile: str,
|
|
2182
|
-
session_profile, # type: ignore[no-untyped-def]
|
|
2183
|
-
context, # type: ignore[no-untyped-def]
|
|
2184
2464
|
app_key: str,
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
requested_focus_field_ids: list[int],
|
|
2188
|
-
workflow_node_id: int | None,
|
|
2189
|
-
warnings: list[JSONObject],
|
|
2465
|
+
record_id: Any,
|
|
2466
|
+
view_id: str | None = None,
|
|
2190
2467
|
) -> JSONObject:
|
|
2191
|
-
"""
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
context,
|
|
2203
|
-
app_key=app_key,
|
|
2204
|
-
record_id=record_id_int,
|
|
2205
|
-
resolved_view=resolved_view,
|
|
2206
|
-
audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
|
|
2207
|
-
)
|
|
2208
|
-
answer_list = _record_detail_answers(detail_result)
|
|
2209
|
-
selected_fields = list(index.by_id.values())
|
|
2210
|
-
row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id_int)
|
|
2211
|
-
normalized_record, _normalized_ambiguous_fields = _build_normalized_row_from_answers(
|
|
2212
|
-
cast(list[JSONValue], answer_list),
|
|
2213
|
-
selected_fields,
|
|
2214
|
-
)
|
|
2215
|
-
if self._record_get_needs_schema_refresh(
|
|
2216
|
-
answer_list=cast(list[JSONValue], answer_list),
|
|
2217
|
-
selected_fields=selected_fields,
|
|
2218
|
-
record=row,
|
|
2219
|
-
normalized_record=normalized_record,
|
|
2220
|
-
):
|
|
2221
|
-
self._clear_record_schema_caches(
|
|
2222
|
-
profile=profile,
|
|
2223
|
-
app_key=app_key,
|
|
2224
|
-
resolved_view=resolved_view,
|
|
2225
|
-
clear_view_caches=True,
|
|
2468
|
+
"""读取单条记录可见的全量数据日志和流程日志,写入本地 JSONL。"""
|
|
2469
|
+
record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
|
|
2470
|
+
if not _normalize_optional_text(view_id):
|
|
2471
|
+
raise_tool_error(
|
|
2472
|
+
QingflowApiError.config_error(
|
|
2473
|
+
"record_logs_get requires view_id. Call app_get first and pass accessible_views[].view_id.",
|
|
2474
|
+
details={
|
|
2475
|
+
"error_code": "RECORD_LOGS_VIEW_REQUIRED",
|
|
2476
|
+
"fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_logs_get with view_id.",
|
|
2477
|
+
},
|
|
2478
|
+
)
|
|
2226
2479
|
)
|
|
2227
|
-
schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=True)
|
|
2228
|
-
index = _build_top_level_field_index(schema)
|
|
2229
|
-
selected_fields = list(index.by_id.values())
|
|
2230
2480
|
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2481
|
+
def runner(session_profile, context):
|
|
2482
|
+
resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
|
|
2483
|
+
profile,
|
|
2484
|
+
context,
|
|
2485
|
+
app_key,
|
|
2486
|
+
view_id=view_id,
|
|
2487
|
+
list_type=None,
|
|
2488
|
+
view_key=None,
|
|
2489
|
+
view_name=None,
|
|
2490
|
+
allow_default=False,
|
|
2491
|
+
)
|
|
2492
|
+
warnings: list[JSONObject] = []
|
|
2493
|
+
warnings.extend(compatibility_warnings)
|
|
2494
|
+
warnings.extend(_view_filter_trust_warnings(resolved_view))
|
|
2495
|
+
unavailable_context: list[JSONObject] = []
|
|
2496
|
+
|
|
2497
|
+
schema: JSONObject = {}
|
|
2498
|
+
try:
|
|
2499
|
+
schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
|
|
2500
|
+
except QingflowApiError as exc:
|
|
2501
|
+
if not _is_optional_schema_permission_error(exc):
|
|
2502
|
+
raise
|
|
2503
|
+
unavailable_context.append(
|
|
2504
|
+
_record_detail_unavailable_context(
|
|
2505
|
+
"detail_schema",
|
|
2506
|
+
"记录日志字段结构辅助信息获取失败,已尝试使用详情主数据中的字段信息继续。",
|
|
2507
|
+
exc,
|
|
2508
|
+
)
|
|
2509
|
+
)
|
|
2510
|
+
index = _build_top_level_field_index(schema)
|
|
2511
|
+
try:
|
|
2512
|
+
audit_info = self._record_get_audit_info(
|
|
2513
|
+
context,
|
|
2514
|
+
app_key=app_key,
|
|
2515
|
+
record_id=record_id_int,
|
|
2516
|
+
resolved_view=resolved_view,
|
|
2517
|
+
)
|
|
2518
|
+
except QingflowApiError as exc:
|
|
2519
|
+
if not _is_optional_schema_permission_error(exc):
|
|
2520
|
+
raise
|
|
2521
|
+
audit_info = {}
|
|
2522
|
+
unavailable_context.append(
|
|
2523
|
+
_record_detail_unavailable_context(
|
|
2524
|
+
"audit_info",
|
|
2525
|
+
"记录审批节点辅助信息获取失败,已继续读取详情主数据和日志。",
|
|
2526
|
+
exc,
|
|
2527
|
+
)
|
|
2528
|
+
)
|
|
2529
|
+
audit_context = _record_detail_audit_context(audit_info, workflow_node_id=None)
|
|
2530
|
+
detail_result, used_list_type, used_role = self._record_get_apply_detail(
|
|
2531
|
+
context,
|
|
2532
|
+
app_key=app_key,
|
|
2533
|
+
record_id=record_id_int,
|
|
2534
|
+
resolved_view=resolved_view,
|
|
2535
|
+
audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
|
|
2536
|
+
)
|
|
2537
|
+
answer_list = _record_detail_answers(detail_result)
|
|
2538
|
+
if not index.by_id:
|
|
2539
|
+
answer_index = _build_answer_backed_field_index(cast(list[JSONObject], answer_list))
|
|
2540
|
+
if answer_index.by_id:
|
|
2541
|
+
index = answer_index
|
|
2542
|
+
unavailable_context.append(
|
|
2543
|
+
{
|
|
2544
|
+
"section": "detail_schema",
|
|
2545
|
+
"message": "字段结构由详情 answers 回退构造;候选范围、只读状态、选项和联动信息可能不完整。",
|
|
2546
|
+
"category": "partial_context",
|
|
2547
|
+
}
|
|
2548
|
+
)
|
|
2549
|
+
selected_fields = list(index.by_id.values())
|
|
2550
|
+
fields = [
|
|
2551
|
+
_record_detail_field_payload(field, _find_answer_for_field(cast(list[JSONValue], answer_list), field), focus_id_set=set())
|
|
2552
|
+
for field in selected_fields
|
|
2553
|
+
]
|
|
2554
|
+
app_name = self._record_get_detail_app_name(
|
|
2555
|
+
profile,
|
|
2556
|
+
context,
|
|
2557
|
+
app_key=app_key,
|
|
2558
|
+
schema=schema,
|
|
2559
|
+
used_list_type=used_list_type,
|
|
2560
|
+
)
|
|
2561
|
+
view_payload = _accessible_view_payload(resolved_view)
|
|
2562
|
+
record_payload = _record_detail_record_payload(
|
|
2563
|
+
app_key=app_key,
|
|
2564
|
+
record_id=record_id_int,
|
|
2565
|
+
detail=detail_result,
|
|
2566
|
+
answer_list=cast(list[JSONValue], answer_list),
|
|
2567
|
+
fields=fields,
|
|
2568
|
+
)
|
|
2569
|
+
log_visibility = self._record_get_log_visibility_context(
|
|
2570
|
+
context,
|
|
2571
|
+
app_key=app_key,
|
|
2572
|
+
record_id=record_id_int,
|
|
2573
|
+
resolved_view=resolved_view,
|
|
2574
|
+
role=used_role,
|
|
2575
|
+
audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
|
|
2576
|
+
unavailable_context=unavailable_context,
|
|
2577
|
+
)
|
|
2578
|
+
run_dir = _record_logs_run_dir()
|
|
2579
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
2580
|
+
deadline = time.monotonic() + RECORD_LOGS_TIME_BUDGET_SECONDS
|
|
2581
|
+
data_logs = self._record_get_full_data_logs_context(
|
|
2582
|
+
context,
|
|
2583
|
+
app_key=app_key,
|
|
2584
|
+
record_id=record_id_int,
|
|
2585
|
+
role=used_role,
|
|
2586
|
+
log_visibility=log_visibility,
|
|
2587
|
+
unavailable_context=unavailable_context,
|
|
2588
|
+
run_dir=run_dir,
|
|
2589
|
+
deadline=deadline,
|
|
2590
|
+
)
|
|
2591
|
+
workflow_logs = self._record_get_full_workflow_logs_context(
|
|
2592
|
+
context,
|
|
2593
|
+
app_key=app_key,
|
|
2594
|
+
record_id=record_id_int,
|
|
2595
|
+
resolved_view=resolved_view,
|
|
2596
|
+
role=used_role,
|
|
2597
|
+
audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
|
|
2598
|
+
log_visibility=log_visibility,
|
|
2599
|
+
unavailable_context=unavailable_context,
|
|
2600
|
+
run_dir=run_dir,
|
|
2601
|
+
deadline=deadline,
|
|
2602
|
+
)
|
|
2603
|
+
status = _record_logs_overall_status(data_logs=data_logs, workflow_logs=workflow_logs)
|
|
2604
|
+
context_integrity = _record_logs_context_integrity(data_logs=data_logs, workflow_logs=workflow_logs)
|
|
2605
|
+
payload: JSONObject = {
|
|
2606
|
+
"ok": True,
|
|
2607
|
+
"status": status,
|
|
2608
|
+
"output_profile": "record_logs",
|
|
2609
|
+
"app": {"app_key": app_key, "app_name": app_name},
|
|
2610
|
+
"view": view_payload,
|
|
2611
|
+
"record": record_payload,
|
|
2612
|
+
"local_dir": str(run_dir),
|
|
2613
|
+
"data_logs": data_logs,
|
|
2614
|
+
"workflow_logs": workflow_logs,
|
|
2615
|
+
"warnings": warnings,
|
|
2616
|
+
"unavailable_context": unavailable_context,
|
|
2617
|
+
"context_integrity": context_integrity,
|
|
2618
|
+
}
|
|
2619
|
+
summary_path = run_dir / "summary.json"
|
|
2620
|
+
summary_payload = deepcopy(payload)
|
|
2621
|
+
summary_payload.pop("request_route", None)
|
|
2622
|
+
summary_path.write_text(json.dumps(summary_payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
2623
|
+
payload["summary_path"] = str(summary_path)
|
|
2624
|
+
return payload
|
|
2625
|
+
|
|
2626
|
+
return self._run_record_tool(profile, runner)
|
|
2627
|
+
|
|
2628
|
+
def _record_get_detail_context(
|
|
2629
|
+
self,
|
|
2630
|
+
*,
|
|
2631
|
+
profile: str,
|
|
2632
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
2633
|
+
context, # type: ignore[no-untyped-def]
|
|
2634
|
+
app_key: str,
|
|
2635
|
+
record_id_int: int,
|
|
2636
|
+
resolved_view: AccessibleViewRoute,
|
|
2637
|
+
requested_focus_field_ids: list[int],
|
|
2638
|
+
workflow_node_id: int | None,
|
|
2639
|
+
warnings: list[JSONObject],
|
|
2640
|
+
) -> JSONObject:
|
|
2641
|
+
"""执行内部辅助逻辑。"""
|
|
2642
|
+
unavailable_context: list[JSONObject] = []
|
|
2643
|
+
schema: JSONObject = {}
|
|
2644
|
+
schema_available = True
|
|
2645
|
+
try:
|
|
2646
|
+
schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
|
|
2647
|
+
except QingflowApiError as exc:
|
|
2648
|
+
if not _is_optional_schema_permission_error(exc):
|
|
2649
|
+
raise
|
|
2650
|
+
schema_available = False
|
|
2651
|
+
unavailable_context.append(
|
|
2652
|
+
_record_detail_unavailable_context(
|
|
2653
|
+
"detail_schema",
|
|
2654
|
+
"记录详情字段结构辅助信息获取失败,已尝试使用详情主数据中的字段信息继续。",
|
|
2655
|
+
exc,
|
|
2656
|
+
)
|
|
2657
|
+
)
|
|
2658
|
+
index = _build_top_level_field_index(schema)
|
|
2659
|
+
try:
|
|
2660
|
+
audit_info = self._record_get_audit_info(
|
|
2661
|
+
context,
|
|
2662
|
+
app_key=app_key,
|
|
2663
|
+
record_id=record_id_int,
|
|
2664
|
+
resolved_view=resolved_view,
|
|
2665
|
+
)
|
|
2666
|
+
except QingflowApiError as exc:
|
|
2667
|
+
if not _is_optional_schema_permission_error(exc):
|
|
2668
|
+
raise
|
|
2669
|
+
audit_info = {}
|
|
2670
|
+
unavailable_context.append(
|
|
2671
|
+
_record_detail_unavailable_context(
|
|
2672
|
+
"audit_info",
|
|
2673
|
+
"记录审批节点辅助信息获取失败,已继续读取详情主数据。",
|
|
2674
|
+
exc,
|
|
2675
|
+
)
|
|
2676
|
+
)
|
|
2677
|
+
audit_context = _record_detail_audit_context(audit_info, workflow_node_id=workflow_node_id)
|
|
2678
|
+
detail_result, used_list_type, used_role = self._record_get_apply_detail(
|
|
2679
|
+
context,
|
|
2680
|
+
app_key=app_key,
|
|
2681
|
+
record_id=record_id_int,
|
|
2682
|
+
resolved_view=resolved_view,
|
|
2683
|
+
audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
|
|
2684
|
+
)
|
|
2685
|
+
answer_list = _record_detail_answers(detail_result)
|
|
2686
|
+
if not index.by_id:
|
|
2687
|
+
answer_index = _build_answer_backed_field_index(cast(list[JSONObject], answer_list))
|
|
2688
|
+
if answer_index.by_id:
|
|
2689
|
+
index = answer_index
|
|
2690
|
+
unavailable_context.append(
|
|
2691
|
+
{
|
|
2692
|
+
"section": "detail_schema",
|
|
2693
|
+
"message": "字段结构由详情 answers 回退构造;候选范围、只读状态、选项和联动信息可能不完整。",
|
|
2694
|
+
"category": "partial_context",
|
|
2695
|
+
}
|
|
2696
|
+
)
|
|
2697
|
+
selected_fields = list(index.by_id.values())
|
|
2698
|
+
row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id_int)
|
|
2699
|
+
normalized_record, _normalized_ambiguous_fields = _build_normalized_row_from_answers(
|
|
2700
|
+
cast(list[JSONValue], answer_list),
|
|
2701
|
+
selected_fields,
|
|
2702
|
+
)
|
|
2703
|
+
if schema_available and self._record_get_needs_schema_refresh(
|
|
2704
|
+
answer_list=cast(list[JSONValue], answer_list),
|
|
2705
|
+
selected_fields=selected_fields,
|
|
2706
|
+
record=row,
|
|
2707
|
+
normalized_record=normalized_record,
|
|
2708
|
+
):
|
|
2709
|
+
self._clear_record_schema_caches(
|
|
2710
|
+
profile=profile,
|
|
2711
|
+
app_key=app_key,
|
|
2712
|
+
resolved_view=resolved_view,
|
|
2713
|
+
clear_view_caches=True,
|
|
2714
|
+
)
|
|
2715
|
+
schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=True)
|
|
2716
|
+
index = _build_top_level_field_index(schema)
|
|
2717
|
+
selected_fields = list(index.by_id.values())
|
|
2718
|
+
|
|
2719
|
+
dynamic_reference_answers, dynamic_reference_unavailable = self._record_get_dynamic_reference_answers(
|
|
2720
|
+
context,
|
|
2721
|
+
app_key=app_key,
|
|
2722
|
+
record_id=record_id_int,
|
|
2723
|
+
resolved_view=resolved_view,
|
|
2724
|
+
role=used_role,
|
|
2725
|
+
audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
|
|
2726
|
+
answer_list=cast(list[JSONValue], answer_list),
|
|
2727
|
+
selected_fields=selected_fields,
|
|
2728
|
+
)
|
|
2729
|
+
unavailable_context.extend(dynamic_reference_unavailable)
|
|
2730
|
+
if dynamic_reference_answers:
|
|
2731
|
+
answer_list = _merge_record_detail_answers(
|
|
2732
|
+
cast(list[JSONValue], answer_list),
|
|
2733
|
+
dynamic_reference_answers,
|
|
2247
2734
|
)
|
|
2248
2735
|
|
|
2249
2736
|
app_name = self._record_get_detail_app_name(
|
|
@@ -2387,7 +2874,20 @@ class RecordTools(ToolBase):
|
|
|
2387
2874
|
) -> JSONObject:
|
|
2388
2875
|
"""执行内部辅助逻辑。"""
|
|
2389
2876
|
if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
2390
|
-
return self.
|
|
2877
|
+
return self._get_custom_view_browse_schema(
|
|
2878
|
+
profile,
|
|
2879
|
+
context,
|
|
2880
|
+
resolved_view.view_selection.view_key,
|
|
2881
|
+
force_refresh=force_refresh,
|
|
2882
|
+
)
|
|
2883
|
+
if resolved_view.kind == "system" and resolved_view.list_type is not None:
|
|
2884
|
+
return self._get_system_browse_schema(
|
|
2885
|
+
profile,
|
|
2886
|
+
context,
|
|
2887
|
+
app_key,
|
|
2888
|
+
list_type=resolved_view.list_type,
|
|
2889
|
+
force_refresh=force_refresh,
|
|
2890
|
+
)
|
|
2391
2891
|
return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
|
|
2392
2892
|
|
|
2393
2893
|
def _record_get_audit_info(
|
|
@@ -2448,7 +2948,7 @@ class RecordTools(ToolBase):
|
|
|
2448
2948
|
)
|
|
2449
2949
|
return result if isinstance(result, dict) else {"value": result}, list_type, role
|
|
2450
2950
|
except QingflowApiError as exc:
|
|
2451
|
-
if resolved_view.list_type is not None or exc
|
|
2951
|
+
if resolved_view.list_type is not None or not _is_record_permission_denied_error(exc):
|
|
2452
2952
|
raise
|
|
2453
2953
|
last_error: QingflowApiError = exc
|
|
2454
2954
|
for fallback_list_type in (14, 1, 2, 12):
|
|
@@ -2466,7 +2966,7 @@ class RecordTools(ToolBase):
|
|
|
2466
2966
|
return result if isinstance(result, dict) else {"value": result}, fallback_list_type, role
|
|
2467
2967
|
except QingflowApiError as fallback_exc:
|
|
2468
2968
|
last_error = fallback_exc
|
|
2469
|
-
if fallback_exc
|
|
2969
|
+
if _is_record_permission_denied_error(fallback_exc):
|
|
2470
2970
|
continue
|
|
2471
2971
|
raise
|
|
2472
2972
|
raise last_error
|
|
@@ -2564,6 +3064,8 @@ class RecordTools(ToolBase):
|
|
|
2564
3064
|
if target_app_key == app_key and str(target_record_id) == str(source_record_id):
|
|
2565
3065
|
reference_payload["self_reference"] = True
|
|
2566
3066
|
except QingflowApiError as exc:
|
|
3067
|
+
if is_auth_like_error(exc):
|
|
3068
|
+
raise
|
|
2567
3069
|
unavailable = _record_detail_unavailable_context(
|
|
2568
3070
|
"reference_detail",
|
|
2569
3071
|
f"引用字段「{field.que_title}」的目标记录详情获取失败。",
|
|
@@ -2661,6 +3163,8 @@ class RecordTools(ToolBase):
|
|
|
2661
3163
|
json_body=body,
|
|
2662
3164
|
)
|
|
2663
3165
|
except QingflowApiError as exc:
|
|
3166
|
+
if is_auth_like_error(exc):
|
|
3167
|
+
raise
|
|
2664
3168
|
unavailable = _record_detail_unavailable_context(
|
|
2665
3169
|
"reference_runtime_match",
|
|
2666
3170
|
"动态引用字段匹配数据获取失败。",
|
|
@@ -2715,6 +3219,8 @@ class RecordTools(ToolBase):
|
|
|
2715
3219
|
},
|
|
2716
3220
|
)
|
|
2717
3221
|
except QingflowApiError as exc:
|
|
3222
|
+
if is_auth_like_error(exc):
|
|
3223
|
+
raise
|
|
2718
3224
|
unavailable_context.append(_record_detail_unavailable_context("log_visibility", "侧边栏日志可见性获取失败。", exc))
|
|
2719
3225
|
return {"status": "unavailable", "channel": channel, "data_log_visible": None, "workflow_log_visible": None}
|
|
2720
3226
|
payload = visibility if isinstance(visibility, dict) else {}
|
|
@@ -2768,6 +3274,8 @@ class RecordTools(ToolBase):
|
|
|
2768
3274
|
source="data_logs",
|
|
2769
3275
|
)
|
|
2770
3276
|
except QingflowApiError as exc:
|
|
3277
|
+
if is_auth_like_error(exc):
|
|
3278
|
+
raise
|
|
2771
3279
|
unavailable_context.append(_record_detail_unavailable_context("data_logs", "最近数据日志获取失败。", exc))
|
|
2772
3280
|
return _record_detail_log_unavailable_payload("data_logs", "fetch_unavailable")
|
|
2773
3281
|
|
|
@@ -2821,9 +3329,117 @@ class RecordTools(ToolBase):
|
|
|
2821
3329
|
source="workflow_logs",
|
|
2822
3330
|
)
|
|
2823
3331
|
except QingflowApiError as exc:
|
|
3332
|
+
if is_auth_like_error(exc):
|
|
3333
|
+
raise
|
|
2824
3334
|
unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "流程日志本次获取失败。", exc))
|
|
2825
3335
|
return _record_detail_log_unavailable_payload("workflow_logs", "fetch_unavailable")
|
|
2826
3336
|
|
|
3337
|
+
def _record_get_full_data_logs_context(
|
|
3338
|
+
self,
|
|
3339
|
+
context, # type: ignore[no-untyped-def]
|
|
3340
|
+
*,
|
|
3341
|
+
app_key: str,
|
|
3342
|
+
record_id: int,
|
|
3343
|
+
role: int,
|
|
3344
|
+
log_visibility: JSONObject,
|
|
3345
|
+
unavailable_context: list[JSONObject],
|
|
3346
|
+
run_dir: Path,
|
|
3347
|
+
deadline: float,
|
|
3348
|
+
) -> JSONObject:
|
|
3349
|
+
"""读取全量数据日志并写入 JSONL。"""
|
|
3350
|
+
if log_visibility.get("status") == "unavailable":
|
|
3351
|
+
return _record_logs_unavailable_payload("data_logs", "visibility_unavailable")
|
|
3352
|
+
if log_visibility.get("data_log_visible") is False:
|
|
3353
|
+
return _record_logs_hidden_payload("data_logs")
|
|
3354
|
+
|
|
3355
|
+
def fetch_page(page_num: int) -> JSONValue:
|
|
3356
|
+
return self.backend.request(
|
|
3357
|
+
"POST",
|
|
3358
|
+
context,
|
|
3359
|
+
f"/worksheet/data/log/{app_key}/{record_id}/page",
|
|
3360
|
+
json_body={
|
|
3361
|
+
"viewChannel": log_visibility.get("channel"),
|
|
3362
|
+
"role": role,
|
|
3363
|
+
"pageNum": page_num,
|
|
3364
|
+
"pageSize": RECORD_LOGS_PAGE_SIZE,
|
|
3365
|
+
},
|
|
3366
|
+
)
|
|
3367
|
+
|
|
3368
|
+
try:
|
|
3369
|
+
return _record_logs_fetch_all_to_jsonl(
|
|
3370
|
+
fetch_page=fetch_page,
|
|
3371
|
+
normalizer=_record_detail_data_log_item,
|
|
3372
|
+
source="data_logs",
|
|
3373
|
+
file_path=run_dir / "data-logs.jsonl",
|
|
3374
|
+
deadline=deadline,
|
|
3375
|
+
)
|
|
3376
|
+
except QingflowApiError as exc:
|
|
3377
|
+
if is_auth_like_error(exc):
|
|
3378
|
+
raise
|
|
3379
|
+
unavailable_context.append(_record_detail_unavailable_context("data_logs", "全量数据日志获取失败。", exc))
|
|
3380
|
+
return _record_logs_unavailable_payload("data_logs", "fetch_unavailable")
|
|
3381
|
+
|
|
3382
|
+
def _record_get_full_workflow_logs_context(
|
|
3383
|
+
self,
|
|
3384
|
+
context, # type: ignore[no-untyped-def]
|
|
3385
|
+
*,
|
|
3386
|
+
app_key: str,
|
|
3387
|
+
record_id: int,
|
|
3388
|
+
resolved_view: AccessibleViewRoute,
|
|
3389
|
+
role: int,
|
|
3390
|
+
audit_node_id: int | None,
|
|
3391
|
+
log_visibility: JSONObject,
|
|
3392
|
+
unavailable_context: list[JSONObject],
|
|
3393
|
+
run_dir: Path,
|
|
3394
|
+
deadline: float,
|
|
3395
|
+
) -> JSONObject:
|
|
3396
|
+
"""读取全量流程日志并写入 JSONL。"""
|
|
3397
|
+
if log_visibility.get("status") == "unavailable":
|
|
3398
|
+
return _record_logs_unavailable_payload("workflow_logs", "visibility_unavailable")
|
|
3399
|
+
if log_visibility.get("workflow_log_visible") is False:
|
|
3400
|
+
return _record_logs_hidden_payload("workflow_logs")
|
|
3401
|
+
|
|
3402
|
+
def fetch_page(page_num: int) -> JSONValue:
|
|
3403
|
+
if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
3404
|
+
return self.backend.request(
|
|
3405
|
+
"POST",
|
|
3406
|
+
context,
|
|
3407
|
+
f"/viewGraph/{resolved_view.view_selection.view_key}/workflow/node/record",
|
|
3408
|
+
json_body={
|
|
3409
|
+
"key": resolved_view.view_selection.view_key,
|
|
3410
|
+
"rowRecordId": str(record_id),
|
|
3411
|
+
"pageNum": page_num,
|
|
3412
|
+
"pageSize": RECORD_LOGS_PAGE_SIZE,
|
|
3413
|
+
},
|
|
3414
|
+
)
|
|
3415
|
+
return self.backend.request(
|
|
3416
|
+
"POST",
|
|
3417
|
+
context,
|
|
3418
|
+
"/application/workflow/node/record",
|
|
3419
|
+
json_body={
|
|
3420
|
+
"key": app_key,
|
|
3421
|
+
"rowRecordId": str(record_id),
|
|
3422
|
+
"nodeId": audit_node_id,
|
|
3423
|
+
"role": role,
|
|
3424
|
+
"pageNum": page_num,
|
|
3425
|
+
"pageSize": RECORD_LOGS_PAGE_SIZE,
|
|
3426
|
+
},
|
|
3427
|
+
)
|
|
3428
|
+
|
|
3429
|
+
try:
|
|
3430
|
+
return _record_logs_fetch_all_to_jsonl(
|
|
3431
|
+
fetch_page=fetch_page,
|
|
3432
|
+
normalizer=_record_detail_workflow_log_item,
|
|
3433
|
+
source="workflow_logs",
|
|
3434
|
+
file_path=run_dir / "workflow-logs.jsonl",
|
|
3435
|
+
deadline=deadline,
|
|
3436
|
+
)
|
|
3437
|
+
except QingflowApiError as exc:
|
|
3438
|
+
if is_auth_like_error(exc):
|
|
3439
|
+
raise
|
|
3440
|
+
unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "全量流程日志获取失败。", exc))
|
|
3441
|
+
return _record_logs_unavailable_payload("workflow_logs", "fetch_unavailable")
|
|
3442
|
+
|
|
2827
3443
|
def _record_get_associated_resources(
|
|
2828
3444
|
self,
|
|
2829
3445
|
context, # type: ignore[no-untyped-def]
|
|
@@ -2853,6 +3469,8 @@ class RecordTools(ToolBase):
|
|
|
2853
3469
|
params["auditNodeId"] = audit_node_id
|
|
2854
3470
|
payload = self.backend.request("GET", context, f"/app/{app_key}/asosChart", params=params)
|
|
2855
3471
|
except QingflowApiError as exc:
|
|
3472
|
+
if is_auth_like_error(exc):
|
|
3473
|
+
raise
|
|
2856
3474
|
unavailable_context.append(_record_detail_unavailable_context("associated_resources", "关联资源获取失败。", exc))
|
|
2857
3475
|
return []
|
|
2858
3476
|
return [_record_detail_associated_resource(item) for item in _record_detail_associated_resource_items(payload)]
|
|
@@ -2890,16 +3508,17 @@ class RecordTools(ToolBase):
|
|
|
2890
3508
|
refresh_source_url=refresh_source_url,
|
|
2891
3509
|
)
|
|
2892
3510
|
except Exception as exc: # defensive: media should never break the core record detail.
|
|
3511
|
+
warning: JSONObject = {
|
|
3512
|
+
"code": "MEDIA_ASSETS_UNAVAILABLE",
|
|
3513
|
+
"message": f"record_get could not collect media assets: {exc}",
|
|
3514
|
+
}
|
|
3515
|
+
if isinstance(exc, QingflowApiError):
|
|
3516
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
2893
3517
|
return {
|
|
2894
3518
|
"status": "unavailable",
|
|
2895
3519
|
"local_dir": None,
|
|
2896
3520
|
"items": [],
|
|
2897
|
-
"warnings": [
|
|
2898
|
-
{
|
|
2899
|
-
"code": "MEDIA_ASSETS_UNAVAILABLE",
|
|
2900
|
-
"message": f"record_get could not collect media assets: {exc}",
|
|
2901
|
-
}
|
|
2902
|
-
],
|
|
3521
|
+
"warnings": [warning],
|
|
2903
3522
|
}
|
|
2904
3523
|
|
|
2905
3524
|
def _record_get_file_assets(
|
|
@@ -2937,16 +3556,17 @@ class RecordTools(ToolBase):
|
|
|
2937
3556
|
refresh_source_url=refresh_source_url,
|
|
2938
3557
|
)
|
|
2939
3558
|
except Exception as exc: # defensive: file assets should never break the core record detail.
|
|
3559
|
+
warning = {
|
|
3560
|
+
"code": "FILE_ASSETS_UNAVAILABLE",
|
|
3561
|
+
"message": f"record_get could not collect file assets: {exc}",
|
|
3562
|
+
}
|
|
3563
|
+
if isinstance(exc, QingflowApiError):
|
|
3564
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
2940
3565
|
return {
|
|
2941
3566
|
"status": "unavailable",
|
|
2942
3567
|
"local_dir": None,
|
|
2943
3568
|
"items": [],
|
|
2944
|
-
"warnings": [
|
|
2945
|
-
{
|
|
2946
|
-
"code": "FILE_ASSETS_UNAVAILABLE",
|
|
2947
|
-
"message": f"record_get could not collect file assets: {exc}",
|
|
2948
|
-
}
|
|
2949
|
-
],
|
|
3569
|
+
"warnings": [warning],
|
|
2950
3570
|
}
|
|
2951
3571
|
|
|
2952
3572
|
def _record_get_refreshed_media_source_url(
|
|
@@ -2958,7 +3578,7 @@ class RecordTools(ToolBase):
|
|
|
2958
3578
|
resolved_view: AccessibleViewRoute,
|
|
2959
3579
|
audit_node_id: int | None,
|
|
2960
3580
|
candidate: JSONObject,
|
|
2961
|
-
) ->
|
|
3581
|
+
) -> JSONValue | None:
|
|
2962
3582
|
"""Refresh the detail payload once to recover an expired attachment storage signature."""
|
|
2963
3583
|
if candidate.get("source") not in {"attachment", "image_field", "subtable"}:
|
|
2964
3584
|
return None
|
|
@@ -2974,8 +3594,15 @@ class RecordTools(ToolBase):
|
|
|
2974
3594
|
resolved_view=resolved_view,
|
|
2975
3595
|
audit_node_id=audit_node_id,
|
|
2976
3596
|
)
|
|
2977
|
-
except QingflowApiError:
|
|
2978
|
-
return
|
|
3597
|
+
except QingflowApiError as exc:
|
|
3598
|
+
return {
|
|
3599
|
+
"source_url": None,
|
|
3600
|
+
"warning": _record_detail_unavailable_context(
|
|
3601
|
+
"asset_url_refresh",
|
|
3602
|
+
"record_get could not refresh the record detail before downloading a private asset.",
|
|
3603
|
+
exc,
|
|
3604
|
+
),
|
|
3605
|
+
}
|
|
2979
3606
|
for answer in _record_detail_answers(detail_result):
|
|
2980
3607
|
if not isinstance(answer, dict) or _coerce_count(answer.get("queId")) != field_id:
|
|
2981
3608
|
continue
|
|
@@ -3490,6 +4117,7 @@ class RecordTools(ToolBase):
|
|
|
3490
4117
|
record_id: Any | None,
|
|
3491
4118
|
fields: JSONObject | None = None,
|
|
3492
4119
|
items: list[JSONObject] | None = None,
|
|
4120
|
+
view_id: str | None = None,
|
|
3493
4121
|
dry_run: bool = False,
|
|
3494
4122
|
verify_write: bool = True,
|
|
3495
4123
|
output_profile: str = "normal",
|
|
@@ -3510,91 +4138,793 @@ class RecordTools(ToolBase):
|
|
|
3510
4138
|
profile=profile,
|
|
3511
4139
|
app_key=app_key,
|
|
3512
4140
|
items=normalized_items,
|
|
4141
|
+
view_id=view_id,
|
|
3513
4142
|
dry_run=dry_run,
|
|
3514
4143
|
verify_write=verify_write,
|
|
3515
4144
|
output_profile=normalized_output_profile,
|
|
3516
4145
|
)
|
|
3517
|
-
if dry_run:
|
|
3518
|
-
raise_tool_error(QingflowApiError.config_error("dry_run currently requires items"))
|
|
3519
|
-
if record_id is None:
|
|
3520
|
-
raise_tool_error(QingflowApiError.config_error("record_id is required"))
|
|
3521
|
-
record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
|
|
3522
|
-
if fields is not None and not isinstance(fields, dict):
|
|
3523
|
-
raise_tool_error(QingflowApiError.config_error("fields must be an object map keyed by field title"))
|
|
3524
|
-
return self._record_update_public_single(
|
|
3525
|
-
profile=profile,
|
|
3526
|
-
app_key=app_key,
|
|
3527
|
-
record_id=record_id_int,
|
|
3528
|
-
fields=cast(JSONObject, fields or {}),
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
4146
|
+
if dry_run:
|
|
4147
|
+
raise_tool_error(QingflowApiError.config_error("dry_run currently requires items"))
|
|
4148
|
+
if record_id is None:
|
|
4149
|
+
raise_tool_error(QingflowApiError.config_error("record_id is required"))
|
|
4150
|
+
record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
|
|
4151
|
+
if fields is not None and not isinstance(fields, dict):
|
|
4152
|
+
raise_tool_error(QingflowApiError.config_error("fields must be an object map keyed by field title"))
|
|
4153
|
+
return self._record_update_public_single(
|
|
4154
|
+
profile=profile,
|
|
4155
|
+
app_key=app_key,
|
|
4156
|
+
record_id=record_id_int,
|
|
4157
|
+
fields=cast(JSONObject, fields or {}),
|
|
4158
|
+
view_id=view_id,
|
|
4159
|
+
verify_write=verify_write,
|
|
4160
|
+
output_profile=normalized_output_profile,
|
|
4161
|
+
)
|
|
4162
|
+
|
|
4163
|
+
def _record_update_public_single(
|
|
4164
|
+
self,
|
|
4165
|
+
*,
|
|
4166
|
+
profile: str,
|
|
4167
|
+
app_key: str,
|
|
4168
|
+
record_id: int,
|
|
4169
|
+
fields: JSONObject,
|
|
4170
|
+
view_id: str | None,
|
|
4171
|
+
verify_write: bool,
|
|
4172
|
+
output_profile: str,
|
|
4173
|
+
capture_exceptions: bool = False,
|
|
4174
|
+
) -> JSONObject:
|
|
4175
|
+
"""执行内部辅助逻辑。"""
|
|
4176
|
+
write_state = {"attempted": False}
|
|
4177
|
+
try:
|
|
4178
|
+
return self._record_update_public_single_impl(
|
|
4179
|
+
profile=profile,
|
|
4180
|
+
app_key=app_key,
|
|
4181
|
+
record_id=record_id,
|
|
4182
|
+
fields=fields,
|
|
4183
|
+
view_id=view_id,
|
|
4184
|
+
verify_write=verify_write,
|
|
4185
|
+
output_profile=output_profile,
|
|
4186
|
+
write_attempted_ref=lambda value: write_state.__setitem__("attempted", value),
|
|
4187
|
+
)
|
|
4188
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
4189
|
+
if not capture_exceptions:
|
|
4190
|
+
raise
|
|
4191
|
+
return self._record_write_exception_response(
|
|
4192
|
+
exc,
|
|
4193
|
+
operation="update",
|
|
4194
|
+
profile=profile,
|
|
4195
|
+
app_key=app_key,
|
|
4196
|
+
record_id=record_id,
|
|
4197
|
+
output_profile=output_profile,
|
|
4198
|
+
human_review=True,
|
|
4199
|
+
write_executed=write_state["attempted"],
|
|
4200
|
+
)
|
|
4201
|
+
|
|
4202
|
+
def _record_update_public_single_impl(
|
|
4203
|
+
self,
|
|
4204
|
+
*,
|
|
4205
|
+
profile: str,
|
|
4206
|
+
app_key: str,
|
|
4207
|
+
record_id: int,
|
|
4208
|
+
fields: JSONObject,
|
|
4209
|
+
view_id: str | None,
|
|
4210
|
+
verify_write: bool,
|
|
4211
|
+
output_profile: str,
|
|
4212
|
+
write_attempted_ref: Callable[[bool], None],
|
|
4213
|
+
) -> JSONObject:
|
|
4214
|
+
"""执行内部辅助逻辑。"""
|
|
4215
|
+
preflight_kwargs: dict[str, Any] = {
|
|
4216
|
+
"profile": profile,
|
|
4217
|
+
"app_key": app_key,
|
|
4218
|
+
"record_id": record_id,
|
|
4219
|
+
"fields": fields,
|
|
4220
|
+
"force_refresh_form": False,
|
|
4221
|
+
}
|
|
4222
|
+
if view_id is not None:
|
|
4223
|
+
preflight_kwargs["preferred_view_id"] = view_id
|
|
4224
|
+
raw_preflight = self._preflight_record_update_with_auto_view(**preflight_kwargs)
|
|
4225
|
+
preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
|
|
4226
|
+
preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
4227
|
+
normalized_payload = self._record_write_normalized_payload(
|
|
4228
|
+
operation="update",
|
|
4229
|
+
record_id=record_id,
|
|
4230
|
+
record_ids=[],
|
|
4231
|
+
normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
|
|
4232
|
+
submit_type=1,
|
|
4233
|
+
selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
|
|
4234
|
+
)
|
|
4235
|
+
if preflight_data.get("blockers"):
|
|
4236
|
+
return self._record_write_blocked_response(
|
|
4237
|
+
raw_preflight,
|
|
4238
|
+
operation="update",
|
|
4239
|
+
normalized_payload=normalized_payload,
|
|
4240
|
+
output_profile=output_profile,
|
|
4241
|
+
human_review=True,
|
|
4242
|
+
target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
|
|
4243
|
+
)
|
|
4244
|
+
write_attempted_ref(True)
|
|
4245
|
+
route_apply, tried_routes, route_blocker = self._record_update_apply_with_auto_route(
|
|
4246
|
+
profile=profile,
|
|
4247
|
+
app_key=app_key,
|
|
4248
|
+
record_id=record_id,
|
|
4249
|
+
normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
|
|
4250
|
+
preflight_data=preflight_data,
|
|
4251
|
+
verify_write=verify_write,
|
|
4252
|
+
force_refresh_form=preflight_used_force_refresh,
|
|
4253
|
+
)
|
|
4254
|
+
if route_blocker is not None:
|
|
4255
|
+
return self._record_update_route_blocked_response(
|
|
4256
|
+
raw_preflight=raw_preflight,
|
|
4257
|
+
operation="update",
|
|
4258
|
+
normalized_payload=normalized_payload,
|
|
4259
|
+
output_profile=output_profile,
|
|
4260
|
+
human_review=True,
|
|
4261
|
+
app_key=app_key,
|
|
4262
|
+
record_id=record_id,
|
|
4263
|
+
tried_routes=tried_routes,
|
|
4264
|
+
route_blocker=route_blocker,
|
|
4265
|
+
)
|
|
4266
|
+
raw_apply = cast(JSONObject, route_apply)
|
|
4267
|
+
return self._record_write_apply_response(
|
|
4268
|
+
raw_apply,
|
|
4269
|
+
operation="update",
|
|
4270
|
+
normalized_payload=normalized_payload,
|
|
4271
|
+
output_profile=output_profile,
|
|
4272
|
+
human_review=True,
|
|
4273
|
+
preflight=raw_preflight,
|
|
4274
|
+
)
|
|
4275
|
+
|
|
4276
|
+
def _record_update_apply_with_auto_route(
|
|
4277
|
+
self,
|
|
4278
|
+
*,
|
|
4279
|
+
profile: str,
|
|
4280
|
+
app_key: str,
|
|
4281
|
+
record_id: int,
|
|
4282
|
+
normalized_answers: list[JSONObject],
|
|
4283
|
+
preflight_data: JSONObject,
|
|
4284
|
+
verify_write: bool,
|
|
4285
|
+
force_refresh_form: bool,
|
|
4286
|
+
) -> tuple[JSONObject | None, list[JSONObject], JSONObject | None]:
|
|
4287
|
+
"""Try record update routes in the same order a frontend user would expect."""
|
|
4288
|
+
tried_routes: list[JSONObject] = []
|
|
4289
|
+
admin_attempt = self._record_update_route_attempt(
|
|
4290
|
+
route_type="admin_direct",
|
|
4291
|
+
endpoint_kind="app_apply_update",
|
|
4292
|
+
role=1,
|
|
4293
|
+
reason="try data-manager direct edit first",
|
|
4294
|
+
)
|
|
4295
|
+
try:
|
|
4296
|
+
raw_apply = self.record_update(
|
|
4297
|
+
profile=profile,
|
|
4298
|
+
app_key=app_key,
|
|
4299
|
+
apply_id=record_id,
|
|
4300
|
+
answers=normalized_answers,
|
|
4301
|
+
fields={},
|
|
4302
|
+
role=1,
|
|
4303
|
+
verify_write=verify_write,
|
|
4304
|
+
force_refresh_form=force_refresh_form,
|
|
4305
|
+
)
|
|
4306
|
+
admin_attempt["status"] = "success"
|
|
4307
|
+
raw_apply["update_route"] = self._record_update_route_public(admin_attempt)
|
|
4308
|
+
raw_apply["tried_routes"] = [admin_attempt]
|
|
4309
|
+
return raw_apply, [admin_attempt], None
|
|
4310
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
4311
|
+
api_error = self._record_update_extract_api_error(exc)
|
|
4312
|
+
if api_error is None or not self._record_update_route_permission_denied(api_error):
|
|
4313
|
+
raise
|
|
4314
|
+
admin_attempt.update(self._record_update_route_error_payload(
|
|
4315
|
+
api_error,
|
|
4316
|
+
status="denied",
|
|
4317
|
+
error_code="ADMIN_UPDATE_PERMISSION_DENIED",
|
|
4318
|
+
))
|
|
4319
|
+
tried_routes.append(admin_attempt)
|
|
4320
|
+
|
|
4321
|
+
view_route = self._record_update_selected_custom_view_route(preflight_data)
|
|
4322
|
+
if view_route is None:
|
|
4323
|
+
tried_routes.append(
|
|
4324
|
+
self._record_update_route_attempt(
|
|
4325
|
+
route_type="view_edit",
|
|
4326
|
+
endpoint_kind="view_apply_update",
|
|
4327
|
+
status="skipped",
|
|
4328
|
+
error_code="VIEW_UPDATE_ROUTE_NOT_AVAILABLE",
|
|
4329
|
+
reason="preflight did not select a single custom view route for this payload",
|
|
4330
|
+
)
|
|
4331
|
+
)
|
|
4332
|
+
else:
|
|
4333
|
+
view_attempt = self._record_update_route_attempt(
|
|
4334
|
+
route_type="view_edit",
|
|
4335
|
+
endpoint_kind="view_apply_update",
|
|
4336
|
+
view_id=cast(str, view_route.get("view_id")),
|
|
4337
|
+
view_key=cast(str, view_route.get("view_key")),
|
|
4338
|
+
view_name=_normalize_optional_text(view_route.get("name")),
|
|
4339
|
+
reason="fallback to frontend custom-view detail edit route",
|
|
4340
|
+
)
|
|
4341
|
+
try:
|
|
4342
|
+
raw_apply = self._record_update_via_custom_view(
|
|
4343
|
+
profile=profile,
|
|
4344
|
+
app_key=app_key,
|
|
4345
|
+
apply_id=record_id,
|
|
4346
|
+
view_key=cast(str, view_route["view_key"]),
|
|
4347
|
+
answers=normalized_answers,
|
|
4348
|
+
verify_write=verify_write,
|
|
4349
|
+
force_refresh_form=force_refresh_form,
|
|
4350
|
+
)
|
|
4351
|
+
view_attempt["status"] = "success"
|
|
4352
|
+
tried_routes.append(view_attempt)
|
|
4353
|
+
raw_apply["update_route"] = self._record_update_route_public(view_attempt)
|
|
4354
|
+
raw_apply["tried_routes"] = tried_routes
|
|
4355
|
+
return raw_apply, tried_routes, None
|
|
4356
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
4357
|
+
api_error = self._record_update_extract_api_error(exc)
|
|
4358
|
+
if api_error is None or not self._record_update_route_permission_denied(api_error):
|
|
4359
|
+
raise
|
|
4360
|
+
view_attempt.update(self._record_update_route_error_payload(
|
|
4361
|
+
api_error,
|
|
4362
|
+
status="denied",
|
|
4363
|
+
error_code="VIEW_UPDATE_PERMISSION_DENIED",
|
|
4364
|
+
))
|
|
4365
|
+
tried_routes.append(view_attempt)
|
|
4366
|
+
|
|
4367
|
+
task_route = self._record_update_task_save_only_candidate(
|
|
4368
|
+
profile=profile,
|
|
4369
|
+
app_key=app_key,
|
|
4370
|
+
record_id=record_id,
|
|
4371
|
+
normalized_answers=normalized_answers,
|
|
4372
|
+
)
|
|
4373
|
+
task_attempt = cast(JSONObject, task_route.get("attempt") if isinstance(task_route.get("attempt"), dict) else {})
|
|
4374
|
+
if not task_route.get("available"):
|
|
4375
|
+
tried_routes.append(task_attempt or self._record_update_route_attempt(
|
|
4376
|
+
route_type="task_save_only",
|
|
4377
|
+
endpoint_kind="workflow_node_save_only",
|
|
4378
|
+
status="skipped",
|
|
4379
|
+
error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
|
|
4380
|
+
reason="no unique current-user todo task can edit the requested fields",
|
|
4381
|
+
))
|
|
4382
|
+
else:
|
|
4383
|
+
task_attempt = self._record_update_route_attempt(
|
|
4384
|
+
route_type="task_save_only",
|
|
4385
|
+
endpoint_kind="workflow_node_save_only",
|
|
4386
|
+
role=3,
|
|
4387
|
+
task_id=_normalize_optional_text(task_route.get("task_id")),
|
|
4388
|
+
workflow_node_id=_coerce_count(task_route.get("workflow_node_id")),
|
|
4389
|
+
reason="fallback to current-user workflow todo save-only route",
|
|
4390
|
+
)
|
|
4391
|
+
try:
|
|
4392
|
+
raw_apply = self._record_update_via_task_save_only(
|
|
4393
|
+
profile=profile,
|
|
4394
|
+
app_key=app_key,
|
|
4395
|
+
apply_id=record_id,
|
|
4396
|
+
workflow_node_id=cast(int, task_route["workflow_node_id"]),
|
|
4397
|
+
answers=normalized_answers,
|
|
4398
|
+
verify_write=verify_write,
|
|
4399
|
+
force_refresh_form=force_refresh_form,
|
|
4400
|
+
)
|
|
4401
|
+
task_attempt["status"] = "success"
|
|
4402
|
+
tried_routes.append(task_attempt)
|
|
4403
|
+
raw_apply["update_route"] = self._record_update_route_public(task_attempt)
|
|
4404
|
+
raw_apply["tried_routes"] = tried_routes
|
|
4405
|
+
return raw_apply, tried_routes, None
|
|
4406
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
4407
|
+
api_error = self._record_update_extract_api_error(exc)
|
|
4408
|
+
if api_error is None or not self._record_update_route_permission_denied(api_error):
|
|
4409
|
+
raise
|
|
4410
|
+
task_attempt.update(self._record_update_route_error_payload(
|
|
4411
|
+
api_error,
|
|
4412
|
+
status="denied",
|
|
4413
|
+
error_code="TASK_UPDATE_PERMISSION_DENIED",
|
|
4414
|
+
))
|
|
4415
|
+
tried_routes.append(task_attempt)
|
|
4416
|
+
return None, tried_routes, {
|
|
4417
|
+
"error_code": "NO_AVAILABLE_UPDATE_ROUTE",
|
|
4418
|
+
"message": "No available record update route could execute this payload for the current user.",
|
|
4419
|
+
"recommended_next_actions": [
|
|
4420
|
+
"If this user should edit the record as a data manager, grant data edit permission and retry record_update.",
|
|
4421
|
+
"If the record is editable from a specific table view in the UI, make sure the target fields are visible and editable in that view.",
|
|
4422
|
+
"If this is workflow work, use task_list -> task_get -> task_action_execute(action='save_only') with the current task context.",
|
|
4423
|
+
],
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
def _record_update_selected_custom_view_route(self, preflight_data: JSONObject) -> JSONObject | None:
|
|
4427
|
+
selection = preflight_data.get("selection")
|
|
4428
|
+
if not isinstance(selection, dict):
|
|
4429
|
+
return None
|
|
4430
|
+
view = selection.get("view")
|
|
4431
|
+
if not isinstance(view, dict):
|
|
4432
|
+
return None
|
|
4433
|
+
view_id = _normalize_optional_text(view.get("view_id"))
|
|
4434
|
+
if not view_id or not view_id.startswith("custom:"):
|
|
4435
|
+
return None
|
|
4436
|
+
view_key = _normalize_optional_text(view.get("view_key")) or view_id.split(":", 1)[1].strip()
|
|
4437
|
+
if not view_key:
|
|
4438
|
+
return None
|
|
4439
|
+
return {
|
|
4440
|
+
"view_id": view_id,
|
|
4441
|
+
"view_key": view_key,
|
|
4442
|
+
"name": view.get("name"),
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
def _record_update_route_attempt(
|
|
4446
|
+
self,
|
|
4447
|
+
*,
|
|
4448
|
+
route_type: str,
|
|
4449
|
+
endpoint_kind: str,
|
|
4450
|
+
status: str = "attempted",
|
|
4451
|
+
role: int | None = None,
|
|
4452
|
+
task_id: str | None = None,
|
|
4453
|
+
workflow_node_id: int | None = None,
|
|
4454
|
+
view_id: str | None = None,
|
|
4455
|
+
view_key: str | None = None,
|
|
4456
|
+
view_name: str | None = None,
|
|
4457
|
+
error_code: str | None = None,
|
|
4458
|
+
reason: str | None = None,
|
|
4459
|
+
) -> JSONObject:
|
|
4460
|
+
payload: JSONObject = {
|
|
4461
|
+
"route_type": route_type,
|
|
4462
|
+
"endpoint_kind": endpoint_kind,
|
|
4463
|
+
"status": status,
|
|
4464
|
+
}
|
|
4465
|
+
if role is not None:
|
|
4466
|
+
payload["role"] = role
|
|
4467
|
+
if task_id:
|
|
4468
|
+
payload["task_id"] = task_id
|
|
4469
|
+
if workflow_node_id is not None:
|
|
4470
|
+
payload["workflow_node_id"] = workflow_node_id
|
|
4471
|
+
if view_id:
|
|
4472
|
+
payload["view_id"] = view_id
|
|
4473
|
+
if view_key:
|
|
4474
|
+
payload["view_key"] = view_key
|
|
4475
|
+
if view_name:
|
|
4476
|
+
payload["view_name"] = view_name
|
|
4477
|
+
if error_code:
|
|
4478
|
+
payload["error_code"] = error_code
|
|
4479
|
+
if reason:
|
|
4480
|
+
payload["reason"] = reason
|
|
4481
|
+
return payload
|
|
4482
|
+
|
|
4483
|
+
def _record_update_route_public(self, attempt: JSONObject) -> JSONObject:
|
|
4484
|
+
return _pick_route_payload(attempt)
|
|
4485
|
+
|
|
4486
|
+
def _record_update_route_error_payload(
|
|
4487
|
+
self,
|
|
4488
|
+
exc: QingflowApiError,
|
|
4489
|
+
*,
|
|
4490
|
+
status: str,
|
|
4491
|
+
error_code: str,
|
|
4492
|
+
) -> JSONObject:
|
|
4493
|
+
payload: JSONObject = {
|
|
4494
|
+
"status": status,
|
|
4495
|
+
"error_code": error_code,
|
|
4496
|
+
"message": exc.message,
|
|
4497
|
+
}
|
|
4498
|
+
if exc.backend_code is not None:
|
|
4499
|
+
payload["backend_code"] = exc.backend_code
|
|
4500
|
+
if exc.http_status is not None:
|
|
4501
|
+
payload["http_status"] = exc.http_status
|
|
4502
|
+
if exc.request_id is not None:
|
|
4503
|
+
payload["request_id"] = exc.request_id
|
|
4504
|
+
return payload
|
|
4505
|
+
|
|
4506
|
+
def _record_update_extract_api_error(self, exc: QingflowApiError | RuntimeError) -> QingflowApiError | None:
|
|
4507
|
+
if isinstance(exc, QingflowApiError):
|
|
4508
|
+
return exc
|
|
4509
|
+
try:
|
|
4510
|
+
payload = json.loads(str(exc))
|
|
4511
|
+
except json.JSONDecodeError:
|
|
4512
|
+
return None
|
|
4513
|
+
if not isinstance(payload, dict):
|
|
4514
|
+
return None
|
|
4515
|
+
return QingflowApiError(
|
|
4516
|
+
category=str(payload.get("category") or "backend"),
|
|
4517
|
+
message=str(payload.get("message") or exc),
|
|
4518
|
+
backend_code=payload.get("backend_code"),
|
|
4519
|
+
request_id=_normalize_optional_text(payload.get("request_id")),
|
|
4520
|
+
http_status=_coerce_count(payload.get("http_status")),
|
|
4521
|
+
details=cast(JSONObject | None, payload.get("details") if isinstance(payload.get("details"), dict) else None),
|
|
4522
|
+
)
|
|
4523
|
+
|
|
4524
|
+
def _record_update_route_permission_denied(self, exc: QingflowApiError) -> bool:
|
|
4525
|
+
if is_auth_like_error(exc):
|
|
4526
|
+
return False
|
|
4527
|
+
if backend_code_int(exc) in {40002, 40027, 40038, 404}:
|
|
4528
|
+
return True
|
|
4529
|
+
if exc.http_status == 404:
|
|
4530
|
+
return True
|
|
4531
|
+
return False
|
|
4532
|
+
|
|
4533
|
+
def _record_update_route_blocked_response(
|
|
4534
|
+
self,
|
|
4535
|
+
*,
|
|
4536
|
+
raw_preflight: JSONObject,
|
|
4537
|
+
operation: str,
|
|
4538
|
+
normalized_payload: JSONObject,
|
|
4539
|
+
output_profile: str,
|
|
4540
|
+
human_review: bool,
|
|
4541
|
+
app_key: str,
|
|
4542
|
+
record_id: int,
|
|
4543
|
+
tried_routes: list[JSONObject],
|
|
4544
|
+
route_blocker: JSONObject,
|
|
4545
|
+
) -> JSONObject:
|
|
4546
|
+
plan_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
4547
|
+
validation = cast(JSONObject, plan_data.get("validation", {}))
|
|
4548
|
+
warnings_payload = validation.get("warnings", [])
|
|
4549
|
+
warnings = [{"code": "PREFLIGHT_WARNING", "message": str(item)} for item in warnings_payload] if isinstance(warnings_payload, list) else []
|
|
4550
|
+
warnings.append(
|
|
4551
|
+
{
|
|
4552
|
+
"code": cast(str, route_blocker.get("error_code") or "NO_AVAILABLE_UPDATE_ROUTE"),
|
|
4553
|
+
"message": cast(str, route_blocker.get("message") or "No update route could execute the write."),
|
|
4554
|
+
}
|
|
4555
|
+
)
|
|
4556
|
+
recommended = list(route_blocker.get("recommended_next_actions") or [])
|
|
4557
|
+
response: JSONObject = {
|
|
4558
|
+
"profile": raw_preflight.get("profile"),
|
|
4559
|
+
"ws_id": raw_preflight.get("ws_id"),
|
|
4560
|
+
"ok": False,
|
|
4561
|
+
"status": "blocked",
|
|
4562
|
+
"write_executed": False,
|
|
4563
|
+
"verification_status": "not_requested",
|
|
4564
|
+
"safe_to_retry": True,
|
|
4565
|
+
"request_route": raw_preflight.get("request_route"),
|
|
4566
|
+
"warnings": warnings,
|
|
4567
|
+
"output_profile": output_profile,
|
|
4568
|
+
"update_route": None,
|
|
4569
|
+
"tried_routes": tried_routes,
|
|
4570
|
+
"error_code": route_blocker.get("error_code"),
|
|
4571
|
+
"data": {
|
|
4572
|
+
"action": {"operation": operation, "executed": False},
|
|
4573
|
+
"resource": {"type": "record", "app_key": app_key, "record_id": stringify_backend_id(record_id), "record_ids": []},
|
|
4574
|
+
"verification": None,
|
|
4575
|
+
"normalized_payload": normalized_payload,
|
|
4576
|
+
"blockers": [route_blocker.get("error_code") or "NO_AVAILABLE_UPDATE_ROUTE"],
|
|
4577
|
+
"field_errors": [],
|
|
4578
|
+
"confirmation_requests": [],
|
|
4579
|
+
"resolved_fields": cast(list[JSONObject], plan_data.get("lookup_resolved_fields", [])),
|
|
4580
|
+
"recommended_next_actions": recommended,
|
|
4581
|
+
"human_review": self._record_write_human_review_payload(operation, enabled=human_review),
|
|
4582
|
+
"error": route_blocker,
|
|
4583
|
+
"update_route": None,
|
|
4584
|
+
"tried_routes": tried_routes,
|
|
4585
|
+
},
|
|
4586
|
+
}
|
|
4587
|
+
if output_profile == "verbose":
|
|
4588
|
+
response["data"]["debug"] = {"preflight": plan_data}
|
|
4589
|
+
return response
|
|
4590
|
+
|
|
4591
|
+
def _record_update_task_save_only_candidate(
|
|
4592
|
+
self,
|
|
4593
|
+
*,
|
|
4594
|
+
profile: str,
|
|
4595
|
+
app_key: str,
|
|
4596
|
+
record_id: int,
|
|
4597
|
+
normalized_answers: list[JSONObject],
|
|
4598
|
+
) -> JSONObject:
|
|
4599
|
+
requested_question_ids = self._record_update_answer_question_ids(normalized_answers)
|
|
4600
|
+
|
|
4601
|
+
def unavailable(*, status: str = "skipped", error_code: str, reason: str, extra: JSONObject | None = None) -> JSONObject:
|
|
4602
|
+
attempt = self._record_update_route_attempt(
|
|
4603
|
+
route_type="task_save_only",
|
|
4604
|
+
endpoint_kind="workflow_node_save_only",
|
|
4605
|
+
status=status,
|
|
4606
|
+
error_code=error_code,
|
|
4607
|
+
reason=reason,
|
|
4608
|
+
)
|
|
4609
|
+
if extra:
|
|
4610
|
+
attempt.update(extra)
|
|
4611
|
+
return {"available": False, "attempt": attempt}
|
|
4612
|
+
|
|
4613
|
+
def runner(session_profile, context):
|
|
4614
|
+
matches: list[JSONObject] = []
|
|
4615
|
+
pages_scanned = 0
|
|
4616
|
+
for page_num in range(1, VERIFY_TASK_FALLBACK_MAX_PAGES + 1):
|
|
4617
|
+
try:
|
|
4618
|
+
task_page = self.backend.request(
|
|
4619
|
+
"POST",
|
|
4620
|
+
context,
|
|
4621
|
+
"/task/dynamic/page",
|
|
4622
|
+
json_body={
|
|
4623
|
+
"type": 1,
|
|
4624
|
+
"processStatus": 1,
|
|
4625
|
+
"appKey": app_key,
|
|
4626
|
+
"pageNum": page_num,
|
|
4627
|
+
"pageSize": VERIFY_TASK_FALLBACK_PAGE_SIZE,
|
|
4628
|
+
},
|
|
4629
|
+
)
|
|
4630
|
+
except QingflowApiError as exc:
|
|
4631
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
4632
|
+
raise
|
|
4633
|
+
return unavailable(
|
|
4634
|
+
error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
|
|
4635
|
+
reason="current-user todo task list is unavailable",
|
|
4636
|
+
extra=self._record_update_route_error_payload(
|
|
4637
|
+
exc,
|
|
4638
|
+
status="skipped",
|
|
4639
|
+
error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
|
|
4640
|
+
),
|
|
4641
|
+
)
|
|
4642
|
+
pages_scanned += 1
|
|
4643
|
+
rows = task_page.get("list") if isinstance(task_page, dict) else None
|
|
4644
|
+
items = [item for item in rows if isinstance(item, dict)] if isinstance(rows, list) else []
|
|
4645
|
+
for item in items:
|
|
4646
|
+
candidate_record_id = _coerce_count(item.get("rowRecordId") or item.get("recordId") or item.get("applyId"))
|
|
4647
|
+
if candidate_record_id == record_id:
|
|
4648
|
+
matches.append(dict(item))
|
|
4649
|
+
if not _page_has_more(cast(JSONObject, task_page if isinstance(task_page, dict) else {}), page_num, VERIFY_TASK_FALLBACK_PAGE_SIZE, len(items)):
|
|
4650
|
+
break
|
|
4651
|
+
|
|
4652
|
+
if not matches:
|
|
4653
|
+
return unavailable(
|
|
4654
|
+
error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
|
|
4655
|
+
reason="no current-user todo task was found for this record",
|
|
4656
|
+
extra={"pages_scanned": pages_scanned},
|
|
4657
|
+
)
|
|
4658
|
+
if len(matches) > 1:
|
|
4659
|
+
return unavailable(
|
|
4660
|
+
error_code="TASK_UPDATE_ROUTE_AMBIGUOUS",
|
|
4661
|
+
reason="multiple current-user todo tasks match this record; refusing to guess workflow context",
|
|
4662
|
+
extra={"matched_tasks": [self._record_update_compact_task_match(item) for item in matches[:5]]},
|
|
4663
|
+
)
|
|
4664
|
+
|
|
4665
|
+
task = matches[0]
|
|
4666
|
+
workflow_node_id = _coerce_count(task.get("nodeId") or task.get("auditNodeId"))
|
|
4667
|
+
if workflow_node_id is None:
|
|
4668
|
+
return unavailable(
|
|
4669
|
+
error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
|
|
4670
|
+
reason="matched todo task does not expose a workflow node id",
|
|
4671
|
+
extra={"matched_task": self._record_update_compact_task_match(task)},
|
|
4672
|
+
)
|
|
4673
|
+
try:
|
|
4674
|
+
editable_payload = self.backend.request(
|
|
4675
|
+
"GET",
|
|
4676
|
+
context,
|
|
4677
|
+
f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
|
|
4678
|
+
)
|
|
4679
|
+
except QingflowApiError as exc:
|
|
4680
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
4681
|
+
raise
|
|
4682
|
+
return unavailable(
|
|
4683
|
+
error_code="TASK_EDITABLE_FIELDS_UNAVAILABLE",
|
|
4684
|
+
reason="workflow node editable field list is unavailable; record_update will not guess task editability",
|
|
4685
|
+
extra=self._record_update_route_error_payload(
|
|
4686
|
+
exc,
|
|
4687
|
+
status="skipped",
|
|
4688
|
+
error_code="TASK_EDITABLE_FIELDS_UNAVAILABLE",
|
|
4689
|
+
),
|
|
4690
|
+
)
|
|
4691
|
+
editable_question_ids = self._record_update_extract_question_ids(editable_payload)
|
|
4692
|
+
if not editable_question_ids:
|
|
4693
|
+
return unavailable(
|
|
4694
|
+
error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
|
|
4695
|
+
reason="workflow node editable field list is empty",
|
|
4696
|
+
extra={
|
|
4697
|
+
"task_id": stringify_backend_id(task.get("id") or task.get("taskId")),
|
|
4698
|
+
"workflow_node_id": workflow_node_id,
|
|
4699
|
+
},
|
|
4700
|
+
)
|
|
4701
|
+
effective_editable_question_ids = self._record_update_effective_task_editable_ids(
|
|
4702
|
+
editable_question_ids,
|
|
4703
|
+
normalized_answers=normalized_answers,
|
|
4704
|
+
)
|
|
4705
|
+
non_editable = sorted(
|
|
4706
|
+
question_id for question_id in requested_question_ids
|
|
4707
|
+
if question_id not in effective_editable_question_ids
|
|
4708
|
+
)
|
|
4709
|
+
if non_editable:
|
|
4710
|
+
return unavailable(
|
|
4711
|
+
status="denied",
|
|
4712
|
+
error_code="TASK_UPDATE_FIELD_NOT_EDITABLE",
|
|
4713
|
+
reason="one or more requested fields are not editable on the current workflow node",
|
|
4714
|
+
extra={
|
|
4715
|
+
"task_id": stringify_backend_id(task.get("id") or task.get("taskId")),
|
|
4716
|
+
"workflow_node_id": workflow_node_id,
|
|
4717
|
+
"non_editable_question_ids": non_editable,
|
|
4718
|
+
},
|
|
4719
|
+
)
|
|
4720
|
+
return {
|
|
4721
|
+
"available": True,
|
|
4722
|
+
"task_id": stringify_backend_id(task.get("id") or task.get("taskId")),
|
|
4723
|
+
"workflow_node_id": workflow_node_id,
|
|
4724
|
+
"matched_task": self._record_update_compact_task_match(task),
|
|
4725
|
+
"editable_question_ids": sorted(editable_question_ids),
|
|
4726
|
+
"effective_editable_question_ids": sorted(effective_editable_question_ids),
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4729
|
+
return self._run_record_tool(profile, runner)
|
|
4730
|
+
|
|
4731
|
+
def _record_update_compact_task_match(self, item: JSONObject) -> JSONObject:
|
|
4732
|
+
return {
|
|
4733
|
+
key: value
|
|
4734
|
+
for key, value in {
|
|
4735
|
+
"task_id": stringify_backend_id(item.get("id") or item.get("taskId")),
|
|
4736
|
+
"record_id": stringify_backend_id(item.get("rowRecordId") or item.get("recordId") or item.get("applyId")),
|
|
4737
|
+
"workflow_node_id": item.get("nodeId") or item.get("auditNodeId"),
|
|
4738
|
+
"workflow_node_name": item.get("nodeName") or item.get("auditNodeName"),
|
|
4739
|
+
}.items()
|
|
4740
|
+
if value not in (None, "", [], {})
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4743
|
+
def _record_update_answer_question_ids(self, answers: list[JSONObject]) -> set[int]:
|
|
4744
|
+
question_ids: set[int] = set()
|
|
4745
|
+
for answer in answers:
|
|
4746
|
+
if not isinstance(answer, dict):
|
|
4747
|
+
continue
|
|
4748
|
+
que_id = _coerce_count(answer.get("queId"))
|
|
4749
|
+
if que_id is not None and que_id > 0:
|
|
4750
|
+
question_ids.add(que_id)
|
|
4751
|
+
table_values = answer.get("tableValues")
|
|
4752
|
+
if not isinstance(table_values, list):
|
|
4753
|
+
continue
|
|
4754
|
+
for row in table_values:
|
|
4755
|
+
if not isinstance(row, list):
|
|
4756
|
+
continue
|
|
4757
|
+
for cell in row:
|
|
4758
|
+
if not isinstance(cell, dict):
|
|
4759
|
+
continue
|
|
4760
|
+
cell_que_id = _coerce_count(cell.get("queId"))
|
|
4761
|
+
if cell_que_id is not None and cell_que_id > 0:
|
|
4762
|
+
question_ids.add(cell_que_id)
|
|
4763
|
+
return question_ids
|
|
4764
|
+
|
|
4765
|
+
def _record_update_extract_question_ids(self, payload: JSONValue) -> set[int]:
|
|
4766
|
+
candidates: list[Any] = []
|
|
4767
|
+
if isinstance(payload, list):
|
|
4768
|
+
candidates = payload
|
|
4769
|
+
elif isinstance(payload, dict):
|
|
4770
|
+
for key in ("editableQueIds", "editableQuestionIds", "queIds", "questionIds", "ids", "list", "result"):
|
|
4771
|
+
value = payload.get(key)
|
|
4772
|
+
if isinstance(value, list):
|
|
4773
|
+
candidates = value
|
|
4774
|
+
break
|
|
4775
|
+
question_ids: set[int] = set()
|
|
4776
|
+
for item in candidates:
|
|
4777
|
+
value: Any = item
|
|
4778
|
+
if isinstance(item, dict):
|
|
4779
|
+
value = item.get("queId", item.get("questionId", item.get("id")))
|
|
4780
|
+
que_id = _coerce_count(value)
|
|
4781
|
+
if que_id is not None and que_id > 0:
|
|
4782
|
+
question_ids.add(que_id)
|
|
4783
|
+
return question_ids
|
|
4784
|
+
|
|
4785
|
+
def _record_update_effective_task_editable_ids(
|
|
4786
|
+
self,
|
|
4787
|
+
editable_question_ids: set[int],
|
|
4788
|
+
*,
|
|
4789
|
+
normalized_answers: list[JSONObject],
|
|
4790
|
+
) -> set[int]:
|
|
4791
|
+
effective_editable_ids = set(editable_question_ids)
|
|
4792
|
+
for answer in normalized_answers:
|
|
4793
|
+
if not isinstance(answer, dict):
|
|
4794
|
+
continue
|
|
4795
|
+
parent_que_id = _coerce_count(answer.get("queId"))
|
|
4796
|
+
if parent_que_id is None or parent_que_id <= 0:
|
|
4797
|
+
continue
|
|
4798
|
+
table_values = answer.get("tableValues")
|
|
4799
|
+
if not isinstance(table_values, list) or not table_values:
|
|
4800
|
+
continue
|
|
4801
|
+
row_subfield_ids: set[int] = set()
|
|
4802
|
+
for row in table_values:
|
|
4803
|
+
if not isinstance(row, list):
|
|
4804
|
+
continue
|
|
4805
|
+
for cell in row:
|
|
4806
|
+
if not isinstance(cell, dict):
|
|
4807
|
+
continue
|
|
4808
|
+
cell_que_id = _coerce_count(cell.get("queId"))
|
|
4809
|
+
if cell_que_id is not None and cell_que_id > 0:
|
|
4810
|
+
row_subfield_ids.add(cell_que_id)
|
|
4811
|
+
if row_subfield_ids & editable_question_ids:
|
|
4812
|
+
effective_editable_ids.add(parent_que_id)
|
|
4813
|
+
return effective_editable_ids
|
|
4814
|
+
|
|
4815
|
+
def _record_update_via_custom_view(
|
|
4816
|
+
self,
|
|
4817
|
+
*,
|
|
4818
|
+
profile: str,
|
|
4819
|
+
app_key: str,
|
|
4820
|
+
apply_id: int,
|
|
4821
|
+
view_key: str,
|
|
4822
|
+
answers: list[JSONObject],
|
|
4823
|
+
verify_write: bool,
|
|
4824
|
+
force_refresh_form: bool,
|
|
4825
|
+
) -> JSONObject:
|
|
4826
|
+
normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
|
|
4827
|
+
normalized_view_key = view_key.strip()
|
|
4828
|
+
if not normalized_view_key:
|
|
4829
|
+
raise_tool_error(QingflowApiError.config_error("view_key is required for custom view update"))
|
|
4830
|
+
|
|
4831
|
+
def runner(session_profile, context):
|
|
4832
|
+
index = self._get_view_field_index(profile, context, normalized_view_key, force_refresh=force_refresh_form) if verify_write else None
|
|
4833
|
+
normalized_answers = [dict(item) for item in answers if isinstance(item, dict)]
|
|
4834
|
+
self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
|
|
4835
|
+
result = self.backend.request(
|
|
4836
|
+
"POST",
|
|
4837
|
+
context,
|
|
4838
|
+
f"/view/{normalized_view_key}/apply/{normalized_apply_id}",
|
|
4839
|
+
json_body={"answers": normalized_answers},
|
|
4840
|
+
)
|
|
4841
|
+
verification = self._verify_record_write_result(
|
|
4842
|
+
context,
|
|
4843
|
+
app_key=app_key,
|
|
4844
|
+
apply_id=normalized_apply_id,
|
|
4845
|
+
normalized_answers=normalized_answers,
|
|
4846
|
+
index=cast(FieldIndex, index),
|
|
4847
|
+
verify_view_key=normalized_view_key,
|
|
4848
|
+
) if verify_write and index is not None else None
|
|
4849
|
+
verified = True if verification is None else bool(verification.get("verified"))
|
|
4850
|
+
return self._attach_human_review_notice(
|
|
4851
|
+
{
|
|
4852
|
+
"profile": profile,
|
|
4853
|
+
"ws_id": session_profile.selected_ws_id,
|
|
4854
|
+
"request_route": self._request_route_payload(context),
|
|
4855
|
+
"app_key": app_key,
|
|
4856
|
+
"apply_id": normalized_apply_id,
|
|
4857
|
+
"record_id": normalized_apply_id,
|
|
4858
|
+
"result": result,
|
|
4859
|
+
"normalized_answers": normalized_answers,
|
|
4860
|
+
"status": "completed" if verified else "verification_failed",
|
|
4861
|
+
"ok": True,
|
|
4862
|
+
"verify_write": verify_write,
|
|
4863
|
+
"write_verified": verified if verify_write else None,
|
|
4864
|
+
"verification": verification,
|
|
4865
|
+
"resource": _record_resource_payload(normalized_apply_id),
|
|
4866
|
+
},
|
|
4867
|
+
operation="update",
|
|
4868
|
+
target="record data",
|
|
4869
|
+
)
|
|
3532
4870
|
|
|
3533
|
-
|
|
4871
|
+
return self._run_record_tool(profile, runner)
|
|
4872
|
+
|
|
4873
|
+
def _record_update_via_task_save_only(
|
|
3534
4874
|
self,
|
|
3535
4875
|
*,
|
|
3536
4876
|
profile: str,
|
|
3537
4877
|
app_key: str,
|
|
3538
|
-
|
|
3539
|
-
|
|
4878
|
+
apply_id: int,
|
|
4879
|
+
workflow_node_id: int,
|
|
4880
|
+
answers: list[JSONObject],
|
|
3540
4881
|
verify_write: bool,
|
|
3541
|
-
|
|
4882
|
+
force_refresh_form: bool,
|
|
3542
4883
|
) -> JSONObject:
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
record_ids=[],
|
|
3557
|
-
normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
|
|
3558
|
-
submit_type=1,
|
|
3559
|
-
selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
|
|
3560
|
-
)
|
|
3561
|
-
if preflight_data.get("blockers"):
|
|
3562
|
-
return self._record_write_blocked_response(
|
|
3563
|
-
raw_preflight,
|
|
3564
|
-
operation="update",
|
|
3565
|
-
normalized_payload=normalized_payload,
|
|
3566
|
-
output_profile=output_profile,
|
|
3567
|
-
human_review=True,
|
|
3568
|
-
target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
|
|
4884
|
+
normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
|
|
4885
|
+
if workflow_node_id <= 0:
|
|
4886
|
+
raise_tool_error(QingflowApiError.config_error("workflow_node_id must be positive for task save-only update"))
|
|
4887
|
+
|
|
4888
|
+
def runner(session_profile, context):
|
|
4889
|
+
index = self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form) if verify_write else None
|
|
4890
|
+
normalized_answers = [dict(item) for item in answers if isinstance(item, dict)]
|
|
4891
|
+
self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
|
|
4892
|
+
result = self.backend.request(
|
|
4893
|
+
"POST",
|
|
4894
|
+
context,
|
|
4895
|
+
f"/app/{app_key}/apply/{normalized_apply_id}",
|
|
4896
|
+
json_body={"role": 3, "auditNodeId": workflow_node_id, "answers": normalized_answers},
|
|
3569
4897
|
)
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
profile=profile,
|
|
4898
|
+
verification = self._verify_record_write_result(
|
|
4899
|
+
context,
|
|
3573
4900
|
app_key=app_key,
|
|
3574
|
-
apply_id=
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
4901
|
+
apply_id=normalized_apply_id,
|
|
4902
|
+
normalized_answers=normalized_answers,
|
|
4903
|
+
index=cast(FieldIndex, index),
|
|
4904
|
+
) if verify_write and index is not None else None
|
|
4905
|
+
verified = True if verification is None else bool(verification.get("verified"))
|
|
4906
|
+
return self._attach_human_review_notice(
|
|
4907
|
+
{
|
|
4908
|
+
"profile": profile,
|
|
4909
|
+
"ws_id": session_profile.selected_ws_id,
|
|
4910
|
+
"request_route": self._request_route_payload(context),
|
|
4911
|
+
"app_key": app_key,
|
|
4912
|
+
"apply_id": normalized_apply_id,
|
|
4913
|
+
"record_id": normalized_apply_id,
|
|
4914
|
+
"result": result,
|
|
4915
|
+
"normalized_answers": normalized_answers,
|
|
4916
|
+
"status": "completed" if verified else "verification_failed",
|
|
4917
|
+
"ok": True,
|
|
4918
|
+
"verify_write": verify_write,
|
|
4919
|
+
"write_verified": verified if verify_write else None,
|
|
4920
|
+
"verification": verification,
|
|
4921
|
+
"resource": _record_resource_payload(normalized_apply_id),
|
|
4922
|
+
},
|
|
3584
4923
|
operation="update",
|
|
3585
|
-
|
|
3586
|
-
record_id=record_id,
|
|
3587
|
-
selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
|
|
4924
|
+
target="record data",
|
|
3588
4925
|
)
|
|
3589
|
-
|
|
3590
|
-
return self.
|
|
3591
|
-
raw_apply,
|
|
3592
|
-
operation="update",
|
|
3593
|
-
normalized_payload=normalized_payload,
|
|
3594
|
-
output_profile=output_profile,
|
|
3595
|
-
human_review=True,
|
|
3596
|
-
preflight=raw_preflight,
|
|
3597
|
-
)
|
|
4926
|
+
|
|
4927
|
+
return self._run_record_tool(profile, runner)
|
|
3598
4928
|
|
|
3599
4929
|
def _record_update_public_batch(
|
|
3600
4930
|
self,
|
|
@@ -3602,6 +4932,7 @@ class RecordTools(ToolBase):
|
|
|
3602
4932
|
profile: str,
|
|
3603
4933
|
app_key: str,
|
|
3604
4934
|
items: list[JSONObject],
|
|
4935
|
+
view_id: str | None,
|
|
3605
4936
|
dry_run: bool,
|
|
3606
4937
|
verify_write: bool,
|
|
3607
4938
|
output_profile: str,
|
|
@@ -3613,6 +4944,7 @@ class RecordTools(ToolBase):
|
|
|
3613
4944
|
app_key=app_key,
|
|
3614
4945
|
record_id=cast(int, item["record_id"]),
|
|
3615
4946
|
fields=cast(JSONObject, item["fields"]),
|
|
4947
|
+
view_id=view_id,
|
|
3616
4948
|
output_profile=output_profile,
|
|
3617
4949
|
)
|
|
3618
4950
|
for item in items
|
|
@@ -3641,8 +4973,10 @@ class RecordTools(ToolBase):
|
|
|
3641
4973
|
app_key=app_key,
|
|
3642
4974
|
record_id=record_id,
|
|
3643
4975
|
fields=fields,
|
|
4976
|
+
view_id=view_id,
|
|
3644
4977
|
verify_write=verify_write,
|
|
3645
4978
|
output_profile=output_profile,
|
|
4979
|
+
capture_exceptions=True,
|
|
3646
4980
|
)
|
|
3647
4981
|
)
|
|
3648
4982
|
except (QingflowApiError, RuntimeError) as exc:
|
|
@@ -3673,16 +5007,20 @@ class RecordTools(ToolBase):
|
|
|
3673
5007
|
app_key: str,
|
|
3674
5008
|
record_id: int,
|
|
3675
5009
|
fields: JSONObject,
|
|
5010
|
+
view_id: str | None,
|
|
3676
5011
|
output_profile: str,
|
|
3677
5012
|
) -> JSONObject:
|
|
3678
5013
|
"""执行内部辅助逻辑。"""
|
|
3679
|
-
|
|
3680
|
-
profile
|
|
3681
|
-
app_key
|
|
3682
|
-
record_id
|
|
3683
|
-
fields
|
|
3684
|
-
force_refresh_form
|
|
3685
|
-
|
|
5014
|
+
preflight_kwargs: dict[str, Any] = {
|
|
5015
|
+
"profile": profile,
|
|
5016
|
+
"app_key": app_key,
|
|
5017
|
+
"record_id": record_id,
|
|
5018
|
+
"fields": fields,
|
|
5019
|
+
"force_refresh_form": False,
|
|
5020
|
+
}
|
|
5021
|
+
if view_id is not None:
|
|
5022
|
+
preflight_kwargs["preferred_view_id"] = view_id
|
|
5023
|
+
raw_preflight = self._preflight_record_update_with_auto_view(**preflight_kwargs)
|
|
3686
5024
|
preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
3687
5025
|
normalized_payload = self._record_write_normalized_payload(
|
|
3688
5026
|
operation="update",
|
|
@@ -3888,11 +5226,20 @@ class RecordTools(ToolBase):
|
|
|
3888
5226
|
item: JSONObject = {
|
|
3889
5227
|
"resource": data.get("resource"),
|
|
3890
5228
|
"status": response.get("status"),
|
|
5229
|
+
"write_executed": bool(response.get("write_executed")),
|
|
5230
|
+
"safe_to_retry": bool(response.get("safe_to_retry", True)),
|
|
5231
|
+
"verification_status": response.get("verification_status", "not_requested"),
|
|
3891
5232
|
"verification": data.get("verification"),
|
|
3892
5233
|
"field_errors": cast(list[JSONObject], data.get("field_errors", [])),
|
|
3893
5234
|
"confirmation_requests": cast(list[JSONObject], data.get("confirmation_requests", [])),
|
|
3894
5235
|
"resolved_fields": cast(list[JSONObject], data.get("resolved_fields", [])),
|
|
3895
5236
|
}
|
|
5237
|
+
update_route = response.get("update_route")
|
|
5238
|
+
if isinstance(update_route, dict):
|
|
5239
|
+
item["update_route"] = update_route
|
|
5240
|
+
tried_routes = response.get("tried_routes")
|
|
5241
|
+
if isinstance(tried_routes, list) and (output_profile == "verbose" or response.get("status") != "success"):
|
|
5242
|
+
item["tried_routes"] = tried_routes
|
|
3896
5243
|
blockers = data.get("blockers")
|
|
3897
5244
|
if isinstance(blockers, list) and blockers:
|
|
3898
5245
|
item["blockers"] = blockers
|
|
@@ -3913,6 +5260,7 @@ class RecordTools(ToolBase):
|
|
|
3913
5260
|
app_key: str,
|
|
3914
5261
|
record_id: int,
|
|
3915
5262
|
fields: JSONObject,
|
|
5263
|
+
preferred_view_id: str | None = None,
|
|
3916
5264
|
force_refresh_form: bool,
|
|
3917
5265
|
) -> JSONObject:
|
|
3918
5266
|
"""执行内部辅助逻辑。"""
|
|
@@ -3920,6 +5268,25 @@ class RecordTools(ToolBase):
|
|
|
3920
5268
|
request_route = self._request_route_payload(context)
|
|
3921
5269
|
def build_once(*, effective_force_refresh: bool) -> JSONObject:
|
|
3922
5270
|
candidate_routes = self._candidate_update_views(profile, context, app_key)
|
|
5271
|
+
normalized_preferred_view_id = _normalize_optional_text(preferred_view_id)
|
|
5272
|
+
if normalized_preferred_view_id:
|
|
5273
|
+
preferred_route = next(
|
|
5274
|
+
(
|
|
5275
|
+
route
|
|
5276
|
+
for route in candidate_routes
|
|
5277
|
+
if route.view_id == normalized_preferred_view_id
|
|
5278
|
+
),
|
|
5279
|
+
None,
|
|
5280
|
+
)
|
|
5281
|
+
if preferred_route is None:
|
|
5282
|
+
raise_tool_error(
|
|
5283
|
+
QingflowApiError.config_error(
|
|
5284
|
+
f"view_id '{normalized_preferred_view_id}' is not an accessible update candidate"
|
|
5285
|
+
)
|
|
5286
|
+
)
|
|
5287
|
+
candidate_routes = [preferred_route]
|
|
5288
|
+
else:
|
|
5289
|
+
candidate_routes = _prefer_custom_update_routes(candidate_routes)
|
|
3923
5290
|
probes = self._probe_candidate_record_contexts(
|
|
3924
5291
|
context,
|
|
3925
5292
|
app_key=app_key,
|
|
@@ -4133,41 +5500,24 @@ class RecordTools(ToolBase):
|
|
|
4133
5500
|
"data": first_confirmation_plan,
|
|
4134
5501
|
}
|
|
4135
5502
|
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
app_key=app_key,
|
|
4140
|
-
record_id=record_id,
|
|
4141
|
-
fields=fields,
|
|
4142
|
-
current_answers=matched_answers_for_union or [],
|
|
4143
|
-
matched_routes=matched_routes,
|
|
4144
|
-
force_refresh_form=effective_force_refresh,
|
|
4145
|
-
)
|
|
4146
|
-
if union_plan is not None:
|
|
4147
|
-
validation = union_plan.get("validation")
|
|
4148
|
-
if isinstance(validation, dict):
|
|
4149
|
-
warnings = validation.get("warnings")
|
|
4150
|
-
if not isinstance(warnings, list):
|
|
4151
|
-
warnings = []
|
|
4152
|
-
validation["warnings"] = warnings
|
|
4153
|
-
for message in fallback_warning_messages:
|
|
4154
|
-
if message not in warnings:
|
|
4155
|
-
warnings.append(message)
|
|
4156
|
-
union_plan["view_probe_summary"] = probe_summary
|
|
4157
|
-
union_plan["record_context_probe"] = probe_summary
|
|
5503
|
+
if normalized_preferred_view_id and first_blocked_plan is not None:
|
|
5504
|
+
first_blocked_plan["view_probe_summary"] = probe_summary
|
|
5505
|
+
first_blocked_plan["record_context_probe"] = probe_summary
|
|
4158
5506
|
return {
|
|
4159
5507
|
"profile": profile,
|
|
4160
5508
|
"ws_id": session_profile.selected_ws_id,
|
|
4161
5509
|
"ok": True,
|
|
4162
5510
|
"request_route": request_route,
|
|
4163
|
-
"data":
|
|
5511
|
+
"data": first_blocked_plan,
|
|
4164
5512
|
}
|
|
4165
5513
|
|
|
4166
5514
|
blocked_data = self._build_auto_view_blocked_preflight_data(
|
|
4167
5515
|
app_key=app_key,
|
|
4168
5516
|
record_id=record_id,
|
|
4169
5517
|
blockers=["NO_SINGLE_VIEW_CAN_UPDATE_ALL_FIELDS"],
|
|
4170
|
-
warnings=[
|
|
5518
|
+
warnings=[
|
|
5519
|
+
"record_update requires one executable frontend route for the full payload; it does not merge writable fields across multiple views."
|
|
5520
|
+
],
|
|
4171
5521
|
recommended_next_actions=[
|
|
4172
5522
|
"Call record_update_schema_get first to inspect the overall writable field set for this record.",
|
|
4173
5523
|
"Reduce the update payload until all requested fields fit inside one matched accessible view.",
|
|
@@ -4214,6 +5564,7 @@ class RecordTools(ToolBase):
|
|
|
4214
5564
|
union_writable_field_ids: set[int] = set()
|
|
4215
5565
|
union_visible_question_ids: set[int] = set()
|
|
4216
5566
|
matched_view_payloads: list[JSONObject] = []
|
|
5567
|
+
union_index: FieldIndex | None = None
|
|
4217
5568
|
|
|
4218
5569
|
for candidate in matched_routes:
|
|
4219
5570
|
browse_scope = self._build_browse_write_scope(
|
|
@@ -4223,11 +5574,13 @@ class RecordTools(ToolBase):
|
|
|
4223
5574
|
candidate,
|
|
4224
5575
|
force_refresh=force_refresh_form,
|
|
4225
5576
|
)
|
|
5577
|
+
browse_index = cast(FieldIndex, browse_scope["index"])
|
|
5578
|
+
union_index = browse_index if union_index is None else _merge_field_indexes(union_index, browse_index)
|
|
4226
5579
|
union_writable_field_ids.update(cast(set[int], browse_scope["writable_field_ids"]))
|
|
4227
5580
|
union_visible_question_ids.update(cast(set[int], browse_scope["visible_question_ids"]))
|
|
4228
5581
|
matched_view_payloads.append(_accessible_view_payload(candidate))
|
|
4229
5582
|
|
|
4230
|
-
if not union_writable_field_ids and not union_visible_question_ids:
|
|
5583
|
+
if union_index is None or (not union_writable_field_ids and not union_visible_question_ids):
|
|
4231
5584
|
return None
|
|
4232
5585
|
|
|
4233
5586
|
plan_data = self._build_record_write_preflight(
|
|
@@ -4244,10 +5597,9 @@ class RecordTools(ToolBase):
|
|
|
4244
5597
|
view_key=None,
|
|
4245
5598
|
view_name=None,
|
|
4246
5599
|
existing_answers_override=current_answers,
|
|
5600
|
+
field_index_override=union_index,
|
|
4247
5601
|
)
|
|
4248
5602
|
|
|
4249
|
-
schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
|
|
4250
|
-
app_index = _build_applicant_top_level_field_index(schema)
|
|
4251
5603
|
validation = cast(JSONObject, plan_data.get("validation", {}))
|
|
4252
5604
|
invalid_fields = cast(list[JSONObject], validation.get("invalid_fields", []))
|
|
4253
5605
|
missing_required_fields = cast(list[JSONObject], validation.get("missing_required_fields", []))
|
|
@@ -4258,12 +5610,21 @@ class RecordTools(ToolBase):
|
|
|
4258
5610
|
invalid_fields.extend(
|
|
4259
5611
|
self._validate_view_scoped_subtable_answers(
|
|
4260
5612
|
normalized_answers=cast(list[JSONObject], plan_data.get("normalized_answers", [])),
|
|
4261
|
-
full_index=
|
|
4262
|
-
selector_index=
|
|
5613
|
+
full_index=union_index,
|
|
5614
|
+
selector_index=union_index,
|
|
4263
5615
|
visible_question_ids=union_visible_question_ids,
|
|
4264
5616
|
)
|
|
4265
5617
|
)
|
|
4266
5618
|
|
|
5619
|
+
readonly_or_system_fields = [
|
|
5620
|
+
item
|
|
5621
|
+
for item in readonly_or_system_fields
|
|
5622
|
+
if not (
|
|
5623
|
+
isinstance(item, dict)
|
|
5624
|
+
and (que_id := _coerce_count(item.get("que_id"))) is not None
|
|
5625
|
+
and que_id in union_writable_field_ids
|
|
5626
|
+
)
|
|
5627
|
+
]
|
|
4267
5628
|
existing_readonly_ids = {
|
|
4268
5629
|
str(_coerce_count(item.get("que_id")))
|
|
4269
5630
|
for item in readonly_or_system_fields
|
|
@@ -4427,7 +5788,13 @@ class RecordTools(ToolBase):
|
|
|
4427
5788
|
view_type=None,
|
|
4428
5789
|
)
|
|
4429
5790
|
)
|
|
4430
|
-
|
|
5791
|
+
try:
|
|
5792
|
+
view_items = self._get_view_list(profile, context, app_key)
|
|
5793
|
+
except QingflowApiError as exc:
|
|
5794
|
+
if not _is_record_permission_denied_error(exc):
|
|
5795
|
+
raise
|
|
5796
|
+
view_items = []
|
|
5797
|
+
for item in view_items:
|
|
4431
5798
|
if not isinstance(item, dict):
|
|
4432
5799
|
continue
|
|
4433
5800
|
view_key = _normalize_optional_text(item.get("viewKey"))
|
|
@@ -4484,7 +5851,9 @@ class RecordTools(ToolBase):
|
|
|
4484
5851
|
return payload
|
|
4485
5852
|
|
|
4486
5853
|
def _is_record_context_route_miss(self, error: QingflowApiError) -> bool:
|
|
4487
|
-
if error
|
|
5854
|
+
if is_auth_like_error(error):
|
|
5855
|
+
return False
|
|
5856
|
+
if backend_code_int(error) in {40002, 40023, 40027, 40038, 404}:
|
|
4488
5857
|
return True
|
|
4489
5858
|
if error.http_status == 404:
|
|
4490
5859
|
return True
|
|
@@ -4514,11 +5883,12 @@ class RecordTools(ToolBase):
|
|
|
4514
5883
|
used_list_type = None
|
|
4515
5884
|
else:
|
|
4516
5885
|
used_list_type = resolved_view.list_type if resolved_view.list_type is not None else DEFAULT_RECORD_LIST_TYPE
|
|
5886
|
+
role = _record_detail_role_for_list_type(used_list_type)
|
|
4517
5887
|
record = self.backend.request(
|
|
4518
5888
|
"GET",
|
|
4519
5889
|
context,
|
|
4520
5890
|
f"/app/{app_key}/apply/{apply_id}",
|
|
4521
|
-
params={"role":
|
|
5891
|
+
params={"role": role, "listType": used_list_type},
|
|
4522
5892
|
)
|
|
4523
5893
|
answers = record.get("answers") if isinstance(record, dict) else None
|
|
4524
5894
|
normalized_answers = [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []
|
|
@@ -4548,6 +5918,8 @@ class RecordTools(ToolBase):
|
|
|
4548
5918
|
error_payload=None,
|
|
4549
5919
|
)
|
|
4550
5920
|
except QingflowApiError as exc:
|
|
5921
|
+
if not self._is_record_context_route_miss(exc):
|
|
5922
|
+
raise
|
|
4551
5923
|
return RecordContextRouteProbe(
|
|
4552
5924
|
route=resolved_view,
|
|
4553
5925
|
answer_list=None,
|
|
@@ -4619,7 +5991,7 @@ class RecordTools(ToolBase):
|
|
|
4619
5991
|
]
|
|
4620
5992
|
|
|
4621
5993
|
def _looks_like_generic_record_update_backend_failure(self, exc: QingflowApiError) -> bool:
|
|
4622
|
-
if exc
|
|
5994
|
+
if backend_code_int(exc) == 500:
|
|
4623
5995
|
return True
|
|
4624
5996
|
if exc.http_status is not None and exc.http_status >= 500:
|
|
4625
5997
|
return True
|
|
@@ -4744,12 +6116,15 @@ class RecordTools(ToolBase):
|
|
|
4744
6116
|
app_key: str,
|
|
4745
6117
|
record_id: Any | None = None,
|
|
4746
6118
|
record_ids: list[Any] | None = None,
|
|
6119
|
+
view_id: str | None = None,
|
|
6120
|
+
list_type: int | None = None,
|
|
4747
6121
|
output_profile: str = "normal",
|
|
4748
6122
|
) -> JSONObject:
|
|
4749
6123
|
"""执行记录相关逻辑。"""
|
|
4750
6124
|
normalized_output_profile = self._normalize_public_output_profile(output_profile)
|
|
4751
6125
|
if not app_key:
|
|
4752
6126
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
6127
|
+
delete_list_type = self._resolve_record_delete_list_type(view_id=view_id, list_type=list_type)
|
|
4753
6128
|
normalized_record_ids: list[int] = []
|
|
4754
6129
|
for index, item in enumerate(record_ids or []):
|
|
4755
6130
|
normalized_record_ids.append(normalize_positive_id_int(item, field_name=f"record_ids[{index}]"))
|
|
@@ -4769,21 +6144,72 @@ class RecordTools(ToolBase):
|
|
|
4769
6144
|
"record_ids": [stringify_backend_id(item) for item in delete_ids],
|
|
4770
6145
|
"answers": [],
|
|
4771
6146
|
"submit_type": 1,
|
|
6147
|
+
"selection": {"view_id": view_id, "list_type": delete_list_type},
|
|
4772
6148
|
}
|
|
4773
6149
|
return self._record_delete_public_batch(
|
|
4774
6150
|
profile=profile,
|
|
4775
6151
|
app_key=app_key,
|
|
4776
6152
|
delete_ids=delete_ids,
|
|
6153
|
+
list_type=delete_list_type,
|
|
4777
6154
|
normalized_payload=normalized_payload,
|
|
4778
6155
|
output_profile=normalized_output_profile,
|
|
4779
6156
|
)
|
|
4780
6157
|
|
|
6158
|
+
def _resolve_record_delete_list_type(self, *, view_id: str | None, list_type: int | None) -> int:
|
|
6159
|
+
normalized_view_id = _normalize_optional_text(view_id)
|
|
6160
|
+
if normalized_view_id:
|
|
6161
|
+
if normalized_view_id.startswith("custom:"):
|
|
6162
|
+
raise_tool_error(
|
|
6163
|
+
QingflowApiError.config_error(
|
|
6164
|
+
"record_delete does not support custom view deletion; the backend delete route accepts system listType only",
|
|
6165
|
+
details={
|
|
6166
|
+
"error_code": "RECORD_DELETE_CUSTOM_VIEW_UNSUPPORTED",
|
|
6167
|
+
"view_id": normalized_view_id,
|
|
6168
|
+
"fix_hint": (
|
|
6169
|
+
"Use a system view_id from app_get.accessible_views, or resolve target record_ids with "
|
|
6170
|
+
"record list/get first and retry delete without a custom view selector."
|
|
6171
|
+
),
|
|
6172
|
+
},
|
|
6173
|
+
)
|
|
6174
|
+
)
|
|
6175
|
+
if not normalized_view_id.startswith("system:"):
|
|
6176
|
+
raise_tool_error(QingflowApiError.config_error("view_id must start with system: or custom:"))
|
|
6177
|
+
mapped_list_type = SYSTEM_VIEW_ID_TO_LIST_TYPE.get(normalized_view_id)
|
|
6178
|
+
if mapped_list_type is None:
|
|
6179
|
+
raise_tool_error(QingflowApiError.config_error(f"unsupported view_id '{normalized_view_id}'"))
|
|
6180
|
+
return mapped_list_type
|
|
6181
|
+
if list_type is not None:
|
|
6182
|
+
normalized_list_type = int(list_type)
|
|
6183
|
+
if normalized_list_type not in SYSTEM_VIEW_LIST_TYPES:
|
|
6184
|
+
raise_tool_error(
|
|
6185
|
+
QingflowApiError.config_error(
|
|
6186
|
+
"record_delete list_type must map to a supported system view",
|
|
6187
|
+
details={
|
|
6188
|
+
"error_code": "RECORD_DELETE_SYSTEM_VIEW_REQUIRED",
|
|
6189
|
+
"list_type": normalized_list_type,
|
|
6190
|
+
"supported_list_types": sorted(SYSTEM_VIEW_LIST_TYPES),
|
|
6191
|
+
"fix_hint": "Pass a system view_id from app_get.accessible_views instead of an arbitrary list_type.",
|
|
6192
|
+
},
|
|
6193
|
+
)
|
|
6194
|
+
)
|
|
6195
|
+
return normalized_list_type
|
|
6196
|
+
raise_tool_error(
|
|
6197
|
+
QingflowApiError.config_error(
|
|
6198
|
+
"record_delete requires a system view_id or list_type; deleting without frontend list context is ambiguous",
|
|
6199
|
+
details={
|
|
6200
|
+
"error_code": "RECORD_DELETE_VIEW_REQUIRED",
|
|
6201
|
+
"fix_hint": "Pass a system view_id from app_get.accessible_views, for example --view-id system:all. If the target came from a custom view, first confirm the record_id, then choose an accessible system view for deletion.",
|
|
6202
|
+
},
|
|
6203
|
+
)
|
|
6204
|
+
)
|
|
6205
|
+
|
|
4781
6206
|
def _record_delete_public_batch(
|
|
4782
6207
|
self,
|
|
4783
6208
|
*,
|
|
4784
6209
|
profile: str,
|
|
4785
6210
|
app_key: str,
|
|
4786
6211
|
delete_ids: list[int],
|
|
6212
|
+
list_type: int,
|
|
4787
6213
|
normalized_payload: JSONObject,
|
|
4788
6214
|
output_profile: str,
|
|
4789
6215
|
) -> JSONObject:
|
|
@@ -4793,7 +6219,7 @@ class RecordTools(ToolBase):
|
|
|
4793
6219
|
for index, delete_id in enumerate(delete_ids):
|
|
4794
6220
|
record_id_text = stringify_backend_id(delete_id)
|
|
4795
6221
|
try:
|
|
4796
|
-
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=[delete_id])
|
|
6222
|
+
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=[delete_id], list_type=list_type)
|
|
4797
6223
|
request_route = cast(JSONObject, raw_apply.get("request_route")) if isinstance(raw_apply.get("request_route"), dict) else request_route
|
|
4798
6224
|
ws_id = raw_apply.get("ws_id", ws_id)
|
|
4799
6225
|
single_payload = {
|
|
@@ -4802,6 +6228,7 @@ class RecordTools(ToolBase):
|
|
|
4802
6228
|
"record_ids": [record_id_text],
|
|
4803
6229
|
"answers": [],
|
|
4804
6230
|
"submit_type": 1,
|
|
6231
|
+
"selection": normalized_payload.get("selection"),
|
|
4805
6232
|
}
|
|
4806
6233
|
single_response = self._record_write_apply_response(
|
|
4807
6234
|
raw_apply,
|
|
@@ -5084,12 +6511,13 @@ class RecordTools(ToolBase):
|
|
|
5084
6511
|
preflight=raw_preflight,
|
|
5085
6512
|
)
|
|
5086
6513
|
|
|
5087
|
-
if
|
|
6514
|
+
if view_key is not None or view_name is not None:
|
|
5088
6515
|
raise_tool_error(
|
|
5089
6516
|
QingflowApiError.config_error(
|
|
5090
|
-
"delete does not
|
|
6517
|
+
"delete does not support custom view selectors; use a system view_id/list_type or resolve target record_ids first"
|
|
5091
6518
|
)
|
|
5092
6519
|
)
|
|
6520
|
+
delete_list_type = self._resolve_record_delete_list_type(view_id=view_id, list_type=list_type)
|
|
5093
6521
|
if normalized_values or normalized_set:
|
|
5094
6522
|
raise_tool_error(QingflowApiError.config_error("delete must not include values or set"))
|
|
5095
6523
|
delete_ids = normalized_record_ids or ([record_id] if record_id is not None and record_id > 0 else [])
|
|
@@ -5101,8 +6529,9 @@ class RecordTools(ToolBase):
|
|
|
5101
6529
|
"record_ids": delete_ids,
|
|
5102
6530
|
"answers": [],
|
|
5103
6531
|
"submit_type": submit_type_value,
|
|
6532
|
+
"selection": {"view_id": view_id, "list_type": delete_list_type},
|
|
5104
6533
|
}
|
|
5105
|
-
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=delete_ids)
|
|
6534
|
+
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=delete_ids, list_type=delete_list_type)
|
|
5106
6535
|
return self._record_write_apply_response(
|
|
5107
6536
|
raw_apply,
|
|
5108
6537
|
operation="delete",
|
|
@@ -5252,7 +6681,9 @@ class RecordTools(ToolBase):
|
|
|
5252
6681
|
or _normalize_optional_text(payload.get("appName"))
|
|
5253
6682
|
or _normalize_optional_text(payload.get("appTitle"))
|
|
5254
6683
|
)
|
|
5255
|
-
except QingflowApiError:
|
|
6684
|
+
except QingflowApiError as exc:
|
|
6685
|
+
if is_auth_like_error(exc):
|
|
6686
|
+
raise
|
|
5256
6687
|
name = None
|
|
5257
6688
|
self._app_name_cache[cache_key] = name
|
|
5258
6689
|
return name
|
|
@@ -5406,7 +6837,9 @@ class RecordTools(ToolBase):
|
|
|
5406
6837
|
try:
|
|
5407
6838
|
result = self.backend.request("GET", context, "/data/baseInfo", params={"queId": field.que_id})
|
|
5408
6839
|
payload = result if isinstance(result, dict) else None
|
|
5409
|
-
except QingflowApiError:
|
|
6840
|
+
except QingflowApiError as exc:
|
|
6841
|
+
if is_auth_like_error(exc):
|
|
6842
|
+
raise
|
|
5410
6843
|
payload = None
|
|
5411
6844
|
self._relation_base_info_cache[cache_key] = payload or {}
|
|
5412
6845
|
return payload
|
|
@@ -5679,6 +7112,26 @@ class RecordTools(ToolBase):
|
|
|
5679
7112
|
or bool(fields)
|
|
5680
7113
|
)
|
|
5681
7114
|
|
|
7115
|
+
def _member_candidate_static_preview_should_use_backend(self, field: FormField) -> bool:
|
|
7116
|
+
"""Return true when the frontend field-scope endpoint is safer than directory expansion."""
|
|
7117
|
+
scope = field.member_select_scope if isinstance(field.member_select_scope, dict) else {}
|
|
7118
|
+
if field.member_select_scope_type != 2:
|
|
7119
|
+
return False
|
|
7120
|
+
return bool(
|
|
7121
|
+
_scope_has_dynamic_or_external(scope)
|
|
7122
|
+
or list(scope.get("depart") or [])
|
|
7123
|
+
or list(scope.get("role") or [])
|
|
7124
|
+
)
|
|
7125
|
+
|
|
7126
|
+
def _department_candidate_static_preview_should_use_backend(self, field: FormField) -> bool:
|
|
7127
|
+
"""Return true when static preview would otherwise need ContactAuth-only directory APIs."""
|
|
7128
|
+
scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
|
|
7129
|
+
if field.dept_select_scope_type != 2:
|
|
7130
|
+
return False
|
|
7131
|
+
if _scope_has_dynamic_or_external(scope):
|
|
7132
|
+
return True
|
|
7133
|
+
return bool(_normalize_bool(scope.get("includeSubDeparts")) or not list(scope.get("depart") or []))
|
|
7134
|
+
|
|
5682
7135
|
def _build_candidate_lookup_state(
|
|
5683
7136
|
self,
|
|
5684
7137
|
profile: str,
|
|
@@ -5697,7 +7150,9 @@ class RecordTools(ToolBase):
|
|
|
5697
7150
|
if apply_id is not None:
|
|
5698
7151
|
try:
|
|
5699
7152
|
base_answers = self._load_record_answers_for_preflight(context, app_key=app_key, apply_id=apply_id)
|
|
5700
|
-
except QingflowApiError:
|
|
7153
|
+
except QingflowApiError as exc:
|
|
7154
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
7155
|
+
raise
|
|
5701
7156
|
context_complete = False
|
|
5702
7157
|
state = LookupResolutionState(
|
|
5703
7158
|
operation="update" if apply_id is not None else "insert",
|
|
@@ -6187,15 +7642,16 @@ class RecordTools(ToolBase):
|
|
|
6187
7642
|
)
|
|
6188
7643
|
if configured_candidate is not None:
|
|
6189
7644
|
self._merge_department_candidate(merged, configured_candidate)
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
7645
|
+
if include_sub:
|
|
7646
|
+
for dept in self._list_departments_by_scope(context, dept_id=dept_id, include_sub_departments=True):
|
|
7647
|
+
normalized = _normalize_candidate_department(
|
|
7648
|
+
dept,
|
|
7649
|
+
source_kind="department",
|
|
7650
|
+
source_id=dept_id,
|
|
7651
|
+
source_value=dept_name,
|
|
7652
|
+
)
|
|
7653
|
+
if normalized is not None:
|
|
7654
|
+
self._merge_department_candidate(merged, normalized)
|
|
6199
7655
|
filtered = _filter_department_candidates(list(merged.values()), keyword)
|
|
6200
7656
|
filtered.sort(key=lambda item: (_normalize_optional_text(item.get("value")) or "", _coerce_count(item.get("id")) or 0))
|
|
6201
7657
|
return filtered
|
|
@@ -7336,22 +8792,10 @@ class RecordTools(ToolBase):
|
|
|
7336
8792
|
field_index_override: FieldIndex | None = None,
|
|
7337
8793
|
) -> JSONObject:
|
|
7338
8794
|
"""执行内部辅助逻辑。"""
|
|
7339
|
-
schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
|
|
7340
|
-
base_index = field_index_override or _build_applicant_top_level_field_index(schema)
|
|
7341
|
-
question_relations = _collect_question_relations(schema)
|
|
7342
|
-
runtime_linked_field_ids = _collect_linked_required_field_ids(question_relations)
|
|
7343
|
-
runtime_linked_field_ids.update(_collect_option_linked_field_ids(base_index))
|
|
7344
|
-
index = base_index
|
|
7345
|
-
if operation == "create" and field_index_override is None:
|
|
7346
|
-
linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
|
|
7347
|
-
schema,
|
|
7348
|
-
linked_field_ids=runtime_linked_field_ids,
|
|
7349
|
-
)
|
|
7350
|
-
index = _merge_field_indexes(base_index, linked_hidden_index)
|
|
7351
8795
|
normalized_fields = fields or {}
|
|
7352
8796
|
normalized_answers_input = answers or []
|
|
7353
8797
|
resolved_view: AccessibleViewRoute | None = None
|
|
7354
|
-
selector_index =
|
|
8798
|
+
selector_index: FieldIndex | None = field_index_override
|
|
7355
8799
|
browse_writable_field_ids: set[int] = set()
|
|
7356
8800
|
visible_question_ids: set[int] = set()
|
|
7357
8801
|
if any(item is not None for item in (view_id, list_type, view_key, view_name)):
|
|
@@ -7377,6 +8821,31 @@ class RecordTools(ToolBase):
|
|
|
7377
8821
|
visible_question_ids = cast(set[int], browse_scope["visible_question_ids"])
|
|
7378
8822
|
else:
|
|
7379
8823
|
compatibility_warnings = []
|
|
8824
|
+
if field_index_override is not None:
|
|
8825
|
+
base_index = field_index_override
|
|
8826
|
+
question_relations: list[JSONObject] = []
|
|
8827
|
+
runtime_linked_field_ids: set[int] = set()
|
|
8828
|
+
index = base_index
|
|
8829
|
+
elif operation == "update" and resolved_view is not None:
|
|
8830
|
+
base_index = cast(FieldIndex, selector_index)
|
|
8831
|
+
question_relations = []
|
|
8832
|
+
runtime_linked_field_ids = set()
|
|
8833
|
+
index = base_index
|
|
8834
|
+
else:
|
|
8835
|
+
schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
|
|
8836
|
+
base_index = _build_applicant_top_level_field_index(schema)
|
|
8837
|
+
question_relations = _collect_question_relations(schema)
|
|
8838
|
+
runtime_linked_field_ids = _collect_linked_required_field_ids(question_relations)
|
|
8839
|
+
runtime_linked_field_ids.update(_collect_option_linked_field_ids(base_index))
|
|
8840
|
+
index = base_index
|
|
8841
|
+
if operation == "create":
|
|
8842
|
+
linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
|
|
8843
|
+
schema,
|
|
8844
|
+
linked_field_ids=runtime_linked_field_ids,
|
|
8845
|
+
)
|
|
8846
|
+
index = _merge_field_indexes(base_index, linked_hidden_index)
|
|
8847
|
+
if selector_index is None:
|
|
8848
|
+
selector_index = index
|
|
7380
8849
|
resolved_fields = self._collect_write_plan_field_refs(fields=normalized_fields, answers=normalized_answers_input, index=selector_index)
|
|
7381
8850
|
support_matrix = _summarize_write_support(resolved_fields)
|
|
7382
8851
|
invalid_fields: list[JSONObject] = []
|
|
@@ -7420,7 +8889,9 @@ class RecordTools(ToolBase):
|
|
|
7420
8889
|
apply_id=apply_id,
|
|
7421
8890
|
)
|
|
7422
8891
|
existing_answers_loaded = True
|
|
7423
|
-
except QingflowApiError:
|
|
8892
|
+
except QingflowApiError as exc:
|
|
8893
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
8894
|
+
raise
|
|
7424
8895
|
validation_warnings.append(
|
|
7425
8896
|
"update preflight could not load the current record; required-field completeness and dynamic lookup context were not fully revalidated."
|
|
7426
8897
|
)
|
|
@@ -8009,7 +9480,7 @@ class RecordTools(ToolBase):
|
|
|
8009
9480
|
break
|
|
8010
9481
|
except QingflowApiError as exc:
|
|
8011
9482
|
last_error = exc
|
|
8012
|
-
if exc
|
|
9483
|
+
if _is_record_permission_denied_error(exc):
|
|
8013
9484
|
continue
|
|
8014
9485
|
raise
|
|
8015
9486
|
if result is None:
|
|
@@ -8112,7 +9583,21 @@ class RecordTools(ToolBase):
|
|
|
8112
9583
|
normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
|
|
8113
9584
|
|
|
8114
9585
|
def runner(session_profile, context):
|
|
8115
|
-
|
|
9586
|
+
needs_index = verify_write or bool(fields) or _answers_need_resolution(answers or [])
|
|
9587
|
+
update_index = None
|
|
9588
|
+
if needs_index:
|
|
9589
|
+
update_index = (
|
|
9590
|
+
self._get_system_browse_field_index(
|
|
9591
|
+
profile,
|
|
9592
|
+
context,
|
|
9593
|
+
app_key,
|
|
9594
|
+
list_type=DEFAULT_RECORD_LIST_TYPE,
|
|
9595
|
+
force_refresh=force_refresh_form,
|
|
9596
|
+
)
|
|
9597
|
+
if role == 1
|
|
9598
|
+
else self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
|
|
9599
|
+
)
|
|
9600
|
+
index = update_index if verify_write else None
|
|
8116
9601
|
normalized_answers = self._resolve_answers(
|
|
8117
9602
|
profile,
|
|
8118
9603
|
context,
|
|
@@ -8120,6 +9605,7 @@ class RecordTools(ToolBase):
|
|
|
8120
9605
|
answers=answers or [],
|
|
8121
9606
|
fields=fields or {},
|
|
8122
9607
|
force_refresh_form=force_refresh_form,
|
|
9608
|
+
field_index_override=update_index,
|
|
8123
9609
|
)
|
|
8124
9610
|
self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
|
|
8125
9611
|
try:
|
|
@@ -8173,13 +9659,14 @@ class RecordTools(ToolBase):
|
|
|
8173
9659
|
def record_delete(self, *, profile: str, app_key: str, apply_id: int, list_type: int) -> JSONObject:
|
|
8174
9660
|
"""执行记录相关逻辑。"""
|
|
8175
9661
|
normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
|
|
9662
|
+
delete_list_type = self._resolve_record_delete_list_type(view_id=None, list_type=list_type)
|
|
8176
9663
|
|
|
8177
9664
|
def runner(session_profile, context):
|
|
8178
9665
|
result = self.backend.request(
|
|
8179
9666
|
"DELETE",
|
|
8180
9667
|
context,
|
|
8181
9668
|
f"/app/{app_key}/apply",
|
|
8182
|
-
json_body={"type":
|
|
9669
|
+
json_body={"type": delete_list_type, "applyIds": [normalized_apply_id]},
|
|
8183
9670
|
)
|
|
8184
9671
|
return self._attach_human_review_notice(
|
|
8185
9672
|
{
|
|
@@ -8188,6 +9675,7 @@ class RecordTools(ToolBase):
|
|
|
8188
9675
|
"request_route": self._request_route_payload(context),
|
|
8189
9676
|
"app_key": app_key,
|
|
8190
9677
|
"apply_id": normalized_apply_id,
|
|
9678
|
+
"list_type": delete_list_type,
|
|
8191
9679
|
"result": result,
|
|
8192
9680
|
},
|
|
8193
9681
|
operation="delete",
|
|
@@ -8232,7 +9720,7 @@ class RecordTools(ToolBase):
|
|
|
8232
9720
|
"GET",
|
|
8233
9721
|
context,
|
|
8234
9722
|
f"/app/{app_key}/apply/{apply_id}",
|
|
8235
|
-
params={"role":
|
|
9723
|
+
params={"role": _record_detail_role_for_list_type(list_type), "listType": list_type},
|
|
8236
9724
|
)
|
|
8237
9725
|
answers = result.get("answers") if isinstance(result, dict) else None
|
|
8238
9726
|
answer_list = answers if isinstance(answers, list) else []
|
|
@@ -8591,7 +10079,7 @@ class RecordTools(ToolBase):
|
|
|
8591
10079
|
used_list_type: int | None = None
|
|
8592
10080
|
if view_selection is not None:
|
|
8593
10081
|
fallback_list_types = [view_route.list_type if view_route.list_type is not None else DEFAULT_RECORD_LIST_TYPE]
|
|
8594
|
-
elif view_route.list_type is not None
|
|
10082
|
+
elif view_route.list_type is not None:
|
|
8595
10083
|
fallback_list_types = [view_route.list_type]
|
|
8596
10084
|
else:
|
|
8597
10085
|
fallback_list_types = [DEFAULT_RECORD_LIST_TYPE, 14, 1, 2, 12]
|
|
@@ -8822,7 +10310,7 @@ class RecordTools(ToolBase):
|
|
|
8822
10310
|
try:
|
|
8823
10311
|
payload = self.backend.request("GET", context, f"/view/{view_key}/viewConfig")
|
|
8824
10312
|
except QingflowApiError as exc:
|
|
8825
|
-
if exc
|
|
10313
|
+
if _is_optional_schema_permission_error(exc):
|
|
8826
10314
|
self._view_config_cache[cache_key] = None
|
|
8827
10315
|
return None
|
|
8828
10316
|
raise
|
|
@@ -8943,7 +10431,12 @@ class RecordTools(ToolBase):
|
|
|
8943
10431
|
)
|
|
8944
10432
|
normalized = _normalize_data_list_base_info_schema(payload)
|
|
8945
10433
|
if not isinstance(normalized.get("formQues"), list) or not normalized.get("formQues"):
|
|
8946
|
-
|
|
10434
|
+
try:
|
|
10435
|
+
return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
|
|
10436
|
+
except QingflowApiError as exc:
|
|
10437
|
+
if not _is_optional_schema_permission_error(exc):
|
|
10438
|
+
raise
|
|
10439
|
+
return normalized
|
|
8947
10440
|
self._form_cache[cache_key] = normalized
|
|
8948
10441
|
return normalized
|
|
8949
10442
|
|
|
@@ -8975,8 +10468,16 @@ class RecordTools(ToolBase):
|
|
|
8975
10468
|
cache_key = (profile, f"view:{view_key}", "browse_view_base_info", None)
|
|
8976
10469
|
if not force_refresh and cache_key in self._form_cache:
|
|
8977
10470
|
return self._form_cache[cache_key]
|
|
8978
|
-
|
|
8979
|
-
|
|
10471
|
+
try:
|
|
10472
|
+
payload = self.backend.request("GET", context, f"/view/{view_key}/apply/baseInfo")
|
|
10473
|
+
normalized = _normalize_data_list_base_info_schema(payload)
|
|
10474
|
+
form_ques = normalized.get("formQues")
|
|
10475
|
+
if not isinstance(form_ques, list) or not form_ques:
|
|
10476
|
+
normalized = self._get_view_form_schema(profile, context, view_key, force_refresh=force_refresh)
|
|
10477
|
+
except QingflowApiError as exc:
|
|
10478
|
+
if not _is_optional_schema_permission_error(exc):
|
|
10479
|
+
raise
|
|
10480
|
+
normalized = self._get_view_form_schema(profile, context, view_key, force_refresh=force_refresh)
|
|
8980
10481
|
self._form_cache[cache_key] = normalized
|
|
8981
10482
|
return normalized
|
|
8982
10483
|
|
|
@@ -9032,22 +10533,6 @@ class RecordTools(ToolBase):
|
|
|
9032
10533
|
force_refresh: bool,
|
|
9033
10534
|
) -> JSONObject:
|
|
9034
10535
|
"""Build the UI/table-view readable field scope from apply/baseInfo."""
|
|
9035
|
-
applicant_index: FieldIndex | None
|
|
9036
|
-
applicant_writable_field_ids: set[int]
|
|
9037
|
-
try:
|
|
9038
|
-
applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
|
|
9039
|
-
except QingflowApiError as exc:
|
|
9040
|
-
if exc.backend_code != 40002:
|
|
9041
|
-
raise
|
|
9042
|
-
applicant_index = None
|
|
9043
|
-
applicant_writable_field_ids = set()
|
|
9044
|
-
else:
|
|
9045
|
-
applicant_writable_field_ids = {
|
|
9046
|
-
field.que_id
|
|
9047
|
-
for field in applicant_index.by_id.values()
|
|
9048
|
-
if bool(self._schema_write_hints(field)["writable"])
|
|
9049
|
-
}
|
|
9050
|
-
|
|
9051
10536
|
if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
9052
10537
|
schema = self._get_custom_view_browse_schema(
|
|
9053
10538
|
profile,
|
|
@@ -9056,6 +10541,16 @@ class RecordTools(ToolBase):
|
|
|
9056
10541
|
force_refresh=force_refresh,
|
|
9057
10542
|
)
|
|
9058
10543
|
index = _build_top_level_field_index(schema)
|
|
10544
|
+
visible_question_ids = {field.que_id for field in index.by_id.values()}
|
|
10545
|
+
return {
|
|
10546
|
+
"index": index,
|
|
10547
|
+
"writable_field_ids": {
|
|
10548
|
+
field.que_id
|
|
10549
|
+
for field in index.by_id.values()
|
|
10550
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
10551
|
+
},
|
|
10552
|
+
"visible_question_ids": visible_question_ids,
|
|
10553
|
+
}
|
|
9059
10554
|
elif resolved_view is not None and resolved_view.kind == "system" and resolved_view.list_type is not None:
|
|
9060
10555
|
schema = self._get_system_browse_base_info_schema(
|
|
9061
10556
|
profile,
|
|
@@ -9065,34 +10560,26 @@ class RecordTools(ToolBase):
|
|
|
9065
10560
|
force_refresh=force_refresh,
|
|
9066
10561
|
)
|
|
9067
10562
|
index = _build_top_level_field_index(schema)
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
index = _build_top_level_field_index({"formQues": [[_form_field_to_question(field) for field in enriched_fields]]})
|
|
10563
|
+
visible_question_ids = {field.que_id for field in index.by_id.values()}
|
|
10564
|
+
return {
|
|
10565
|
+
"index": index,
|
|
10566
|
+
"writable_field_ids": {
|
|
10567
|
+
field.que_id
|
|
10568
|
+
for field in index.by_id.values()
|
|
10569
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
10570
|
+
},
|
|
10571
|
+
"visible_question_ids": visible_question_ids,
|
|
10572
|
+
}
|
|
9079
10573
|
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
10574
|
+
applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
|
|
10575
|
+
visible_question_ids = {field.que_id for field in applicant_index.by_id.values()}
|
|
10576
|
+
return {
|
|
10577
|
+
"index": applicant_index,
|
|
10578
|
+
"writable_field_ids": {
|
|
9083
10579
|
field.que_id
|
|
9084
|
-
for field in
|
|
10580
|
+
for field in applicant_index.by_id.values()
|
|
9085
10581
|
if bool(self._schema_write_hints(field)["writable"])
|
|
9086
|
-
}
|
|
9087
|
-
else:
|
|
9088
|
-
writable_field_ids = {
|
|
9089
|
-
field_id
|
|
9090
|
-
for field_id in visible_question_ids
|
|
9091
|
-
if field_id in applicant_writable_field_ids
|
|
9092
|
-
}
|
|
9093
|
-
return {
|
|
9094
|
-
"index": index,
|
|
9095
|
-
"writable_field_ids": writable_field_ids,
|
|
10582
|
+
},
|
|
9096
10583
|
"visible_question_ids": visible_question_ids,
|
|
9097
10584
|
}
|
|
9098
10585
|
|
|
@@ -9106,23 +10593,13 @@ class RecordTools(ToolBase):
|
|
|
9106
10593
|
force_refresh: bool,
|
|
9107
10594
|
) -> JSONObject:
|
|
9108
10595
|
"""执行内部辅助逻辑。"""
|
|
9109
|
-
applicant_index: FieldIndex | None
|
|
9110
|
-
applicant_writable_field_ids: set[int]
|
|
9111
|
-
try:
|
|
9112
|
-
applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
|
|
9113
|
-
except QingflowApiError as exc:
|
|
9114
|
-
if exc.backend_code != 40002:
|
|
9115
|
-
raise
|
|
9116
|
-
applicant_index = None
|
|
9117
|
-
applicant_writable_field_ids = set()
|
|
9118
|
-
else:
|
|
9119
|
-
applicant_writable_field_ids = {
|
|
9120
|
-
field.que_id
|
|
9121
|
-
for field in applicant_index.by_id.values()
|
|
9122
|
-
if bool(self._schema_write_hints(field)["writable"])
|
|
9123
|
-
}
|
|
9124
10596
|
if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
9125
|
-
schema = self.
|
|
10597
|
+
schema = self._get_custom_view_browse_schema(
|
|
10598
|
+
profile,
|
|
10599
|
+
context,
|
|
10600
|
+
resolved_view.view_selection.view_key,
|
|
10601
|
+
force_refresh=force_refresh,
|
|
10602
|
+
)
|
|
9126
10603
|
index = _build_top_level_field_index(schema)
|
|
9127
10604
|
visible_question_ids = self._get_view_question_ids(profile, context, resolved_view.view_selection.view_key)
|
|
9128
10605
|
if not visible_question_ids:
|
|
@@ -9138,6 +10615,12 @@ class RecordTools(ToolBase):
|
|
|
9138
10615
|
index = _build_top_level_field_index(schema)
|
|
9139
10616
|
visible_question_ids = _question_ids_from_schema(schema)
|
|
9140
10617
|
else:
|
|
10618
|
+
applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
|
|
10619
|
+
applicant_writable_field_ids = {
|
|
10620
|
+
field.que_id
|
|
10621
|
+
for field in applicant_index.by_id.values()
|
|
10622
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
10623
|
+
}
|
|
9141
10624
|
index = applicant_index or _build_top_level_field_index(
|
|
9142
10625
|
self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
|
|
9143
10626
|
)
|
|
@@ -9156,43 +10639,13 @@ class RecordTools(ToolBase):
|
|
|
9156
10639
|
"visible_question_ids": set(visible_question_ids),
|
|
9157
10640
|
}
|
|
9158
10641
|
|
|
9159
|
-
if applicant_index is None:
|
|
9160
|
-
return {
|
|
9161
|
-
"index": index,
|
|
9162
|
-
"writable_field_ids": {
|
|
9163
|
-
field.que_id
|
|
9164
|
-
for field in index.by_id.values()
|
|
9165
|
-
if bool(self._schema_write_hints(field)["writable"])
|
|
9166
|
-
},
|
|
9167
|
-
"visible_question_ids": visible_question_ids,
|
|
9168
|
-
}
|
|
9169
|
-
|
|
9170
|
-
augmented_fields = [
|
|
9171
|
-
_clone_form_field(applicant_index.by_id.get(str(field.que_id)) or field)
|
|
9172
|
-
for field in index.by_id.values()
|
|
9173
|
-
]
|
|
9174
|
-
augmented_field_ids = {field.que_id for field in augmented_fields}
|
|
9175
|
-
writable_field_ids = {
|
|
9176
|
-
field_id
|
|
9177
|
-
for field_id in visible_question_ids
|
|
9178
|
-
if field_id in applicant_writable_field_ids
|
|
9179
|
-
}
|
|
9180
|
-
for field in applicant_index.by_id.values():
|
|
9181
|
-
descendant_ids = _subtable_descendant_ids(field)
|
|
9182
|
-
field_visible = field.que_id in visible_question_ids
|
|
9183
|
-
descendant_visible = bool(descendant_ids and (descendant_ids & visible_question_ids))
|
|
9184
|
-
if not field_visible and not descendant_visible:
|
|
9185
|
-
continue
|
|
9186
|
-
if field.que_id not in augmented_field_ids:
|
|
9187
|
-
augmented_fields.append(_clone_form_field(field))
|
|
9188
|
-
augmented_field_ids.add(field.que_id)
|
|
9189
|
-
if descendant_visible:
|
|
9190
|
-
visible_question_ids.add(field.que_id)
|
|
9191
|
-
if field.que_id in applicant_writable_field_ids and (field_visible or descendant_visible):
|
|
9192
|
-
writable_field_ids.add(field.que_id)
|
|
9193
10642
|
return {
|
|
9194
|
-
"index":
|
|
9195
|
-
"writable_field_ids":
|
|
10643
|
+
"index": index,
|
|
10644
|
+
"writable_field_ids": {
|
|
10645
|
+
field.que_id
|
|
10646
|
+
for field in index.by_id.values()
|
|
10647
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
10648
|
+
},
|
|
9196
10649
|
"visible_question_ids": visible_question_ids,
|
|
9197
10650
|
}
|
|
9198
10651
|
|
|
@@ -9257,7 +10710,7 @@ class RecordTools(ToolBase):
|
|
|
9257
10710
|
try:
|
|
9258
10711
|
payload = self.backend.request("GET", context, f"/view/{view_key}/question")
|
|
9259
10712
|
except QingflowApiError as exc:
|
|
9260
|
-
if exc
|
|
10713
|
+
if _is_record_permission_denied_error(exc):
|
|
9261
10714
|
return set()
|
|
9262
10715
|
raise
|
|
9263
10716
|
if not isinstance(payload, list):
|
|
@@ -9301,7 +10754,7 @@ class RecordTools(ToolBase):
|
|
|
9301
10754
|
)
|
|
9302
10755
|
return True
|
|
9303
10756
|
except QingflowApiError as exc:
|
|
9304
|
-
if exc
|
|
10757
|
+
if _is_record_permission_denied_error(exc):
|
|
9305
10758
|
return False
|
|
9306
10759
|
raise
|
|
9307
10760
|
|
|
@@ -9454,7 +10907,12 @@ class RecordTools(ToolBase):
|
|
|
9454
10907
|
requested_name = _normalize_optional_text(view_name)
|
|
9455
10908
|
if requested_key is None and requested_name is None:
|
|
9456
10909
|
return None
|
|
9457
|
-
|
|
10910
|
+
try:
|
|
10911
|
+
views = self._get_view_list(profile, context, app_key)
|
|
10912
|
+
except QingflowApiError as exc:
|
|
10913
|
+
if requested_key is None or not _is_record_permission_denied_error(exc):
|
|
10914
|
+
raise
|
|
10915
|
+
views = []
|
|
9458
10916
|
selected: JSONObject | None = None
|
|
9459
10917
|
if requested_key is not None:
|
|
9460
10918
|
selected = next((item for item in views if _normalize_optional_text(item.get("viewKey")) == requested_key), None)
|
|
@@ -9652,9 +11110,11 @@ class RecordTools(ToolBase):
|
|
|
9652
11110
|
|
|
9653
11111
|
def _should_retry_list_type_fallback(self, error: QingflowApiError) -> bool:
|
|
9654
11112
|
"""执行内部辅助逻辑。"""
|
|
9655
|
-
if error
|
|
11113
|
+
if is_auth_like_error(error):
|
|
11114
|
+
return False
|
|
11115
|
+
if backend_code_int(error) in {40002, 40027, 404}:
|
|
9656
11116
|
return True
|
|
9657
|
-
if error.http_status
|
|
11117
|
+
if error.http_status == 404:
|
|
9658
11118
|
return True
|
|
9659
11119
|
return False
|
|
9660
11120
|
|
|
@@ -10475,6 +11935,70 @@ class RecordTools(ToolBase):
|
|
|
10475
11935
|
)
|
|
10476
11936
|
)
|
|
10477
11937
|
|
|
11938
|
+
def _candidate_lookup_failed_response(
|
|
11939
|
+
self,
|
|
11940
|
+
*,
|
|
11941
|
+
profile: str,
|
|
11942
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
11943
|
+
context, # type: ignore[no-untyped-def]
|
|
11944
|
+
kind: str,
|
|
11945
|
+
error: RecordInputError,
|
|
11946
|
+
field: FormField,
|
|
11947
|
+
app_key: str,
|
|
11948
|
+
record_id_text: str | None,
|
|
11949
|
+
workflow_node_id: int | None,
|
|
11950
|
+
fields_present: bool,
|
|
11951
|
+
keyword: str,
|
|
11952
|
+
scope_source: str,
|
|
11953
|
+
) -> JSONObject:
|
|
11954
|
+
"""Return a structured result when an optional field candidate lookup is unavailable."""
|
|
11955
|
+
error_payload = error.to_dict()
|
|
11956
|
+
error_details = error_payload.get("details") if isinstance(error_payload.get("details"), dict) else {}
|
|
11957
|
+
candidate_error = error_details.get("candidate_error") if isinstance(error_details.get("candidate_error"), dict) else {}
|
|
11958
|
+
warning_transport = {
|
|
11959
|
+
key: candidate_error.get(key)
|
|
11960
|
+
for key in ("backend_code", "http_status", "request_id")
|
|
11961
|
+
if candidate_error.get(key) is not None
|
|
11962
|
+
}
|
|
11963
|
+
selection: JSONObject = {
|
|
11964
|
+
"app_key": app_key,
|
|
11965
|
+
"field_id": field.que_id,
|
|
11966
|
+
"field_title": field.que_title,
|
|
11967
|
+
"record_id": record_id_text,
|
|
11968
|
+
"workflow_node_id": workflow_node_id,
|
|
11969
|
+
"fields_present": fields_present,
|
|
11970
|
+
"keyword": keyword,
|
|
11971
|
+
"permission_scope": "applicant_node",
|
|
11972
|
+
}
|
|
11973
|
+
return {
|
|
11974
|
+
"profile": profile,
|
|
11975
|
+
"ws_id": session_profile.selected_ws_id,
|
|
11976
|
+
"ok": False,
|
|
11977
|
+
"status": "failed",
|
|
11978
|
+
"error_code": error.error_code,
|
|
11979
|
+
"message": error.message,
|
|
11980
|
+
"request_route": self._request_route_payload(context),
|
|
11981
|
+
"warnings": [
|
|
11982
|
+
{
|
|
11983
|
+
"code": error.error_code,
|
|
11984
|
+
"message": error.fix_hint,
|
|
11985
|
+
"kind": kind,
|
|
11986
|
+
"field_id": field.que_id,
|
|
11987
|
+
"field_title": field.que_title,
|
|
11988
|
+
**warning_transport,
|
|
11989
|
+
}
|
|
11990
|
+
],
|
|
11991
|
+
"output_profile": "normal",
|
|
11992
|
+
"data": {
|
|
11993
|
+
"items": [],
|
|
11994
|
+
"pagination": {"returned_items": 0},
|
|
11995
|
+
"selection": selection,
|
|
11996
|
+
"scope_source": scope_source,
|
|
11997
|
+
"fix_hint": error.fix_hint,
|
|
11998
|
+
},
|
|
11999
|
+
"details": error_details,
|
|
12000
|
+
}
|
|
12001
|
+
|
|
10478
12002
|
def _request_route_payload(self, context) -> JSONObject: # type: ignore[no-untyped-def]
|
|
10479
12003
|
"""执行内部辅助逻辑。"""
|
|
10480
12004
|
describe_route = getattr(self.backend, "describe_route", None)
|
|
@@ -10648,7 +12172,7 @@ class RecordTools(ToolBase):
|
|
|
10648
12172
|
selection: JSONObject | None,
|
|
10649
12173
|
) -> None:
|
|
10650
12174
|
"""执行内部辅助逻辑。"""
|
|
10651
|
-
if exc
|
|
12175
|
+
if not _is_record_permission_denied_error(exc):
|
|
10652
12176
|
raise exc
|
|
10653
12177
|
raise_tool_error(
|
|
10654
12178
|
QingflowApiError(
|
|
@@ -10812,6 +12336,9 @@ class RecordTools(ToolBase):
|
|
|
10812
12336
|
response_status = "verification_failed" if verification_status == "failed" else "success"
|
|
10813
12337
|
if not bool(raw_apply.get("ok", True)) and verification_status != "failed":
|
|
10814
12338
|
response_status = raw_status or "failed"
|
|
12339
|
+
update_route = raw_apply.get("update_route") if isinstance(raw_apply.get("update_route"), dict) else None
|
|
12340
|
+
tried_routes = raw_apply.get("tried_routes") if isinstance(raw_apply.get("tried_routes"), list) else []
|
|
12341
|
+
expose_tried_routes = output_profile == "verbose" or response_status != "success"
|
|
10815
12342
|
response: JSONObject = {
|
|
10816
12343
|
"profile": raw_apply.get("profile"),
|
|
10817
12344
|
"ws_id": raw_apply.get("ws_id"),
|
|
@@ -10823,6 +12350,7 @@ class RecordTools(ToolBase):
|
|
|
10823
12350
|
"request_route": raw_apply.get("request_route"),
|
|
10824
12351
|
"warnings": warnings,
|
|
10825
12352
|
"output_profile": output_profile,
|
|
12353
|
+
"update_route": update_route,
|
|
10826
12354
|
"data": {
|
|
10827
12355
|
"action": {"operation": operation, "executed": True},
|
|
10828
12356
|
"resource": resource,
|
|
@@ -10833,8 +12361,12 @@ class RecordTools(ToolBase):
|
|
|
10833
12361
|
"confirmation_requests": [],
|
|
10834
12362
|
"resolved_fields": resolved_fields,
|
|
10835
12363
|
"human_review": self._record_write_human_review_payload(operation, enabled=human_review),
|
|
12364
|
+
"update_route": update_route,
|
|
10836
12365
|
},
|
|
10837
12366
|
}
|
|
12367
|
+
if expose_tried_routes:
|
|
12368
|
+
response["tried_routes"] = tried_routes
|
|
12369
|
+
response["data"]["tried_routes"] = tried_routes
|
|
10838
12370
|
if record_id is not None:
|
|
10839
12371
|
response["record_id"] = record_id
|
|
10840
12372
|
if apply_id is not None:
|
|
@@ -11014,7 +12546,7 @@ class RecordTools(ToolBase):
|
|
|
11014
12546
|
)
|
|
11015
12547
|
return errors
|
|
11016
12548
|
|
|
11017
|
-
def _record_delete_many(self, *, profile: str, app_key: str, record_ids: list[int]) -> JSONObject:
|
|
12549
|
+
def _record_delete_many(self, *, profile: str, app_key: str, record_ids: list[int], list_type: int = DEFAULT_RECORD_LIST_TYPE) -> JSONObject:
|
|
11018
12550
|
"""执行内部辅助逻辑。"""
|
|
11019
12551
|
if not app_key:
|
|
11020
12552
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
@@ -11027,14 +12559,14 @@ class RecordTools(ToolBase):
|
|
|
11027
12559
|
"DELETE",
|
|
11028
12560
|
context,
|
|
11029
12561
|
f"/app/{app_key}/apply",
|
|
11030
|
-
json_body={"type":
|
|
12562
|
+
json_body={"type": list_type, "applyIds": normalized_ids},
|
|
11031
12563
|
)
|
|
11032
12564
|
return {
|
|
11033
12565
|
"profile": profile,
|
|
11034
12566
|
"ws_id": session_profile.selected_ws_id,
|
|
11035
12567
|
"request_route": self._request_route_payload(context),
|
|
11036
12568
|
"result": result,
|
|
11037
|
-
"resource": {"type": "record", "apply_ids": normalized_ids},
|
|
12569
|
+
"resource": {"type": "record", "apply_ids": normalized_ids, "list_type": list_type},
|
|
11038
12570
|
"ok": True,
|
|
11039
12571
|
}
|
|
11040
12572
|
|
|
@@ -11608,6 +13140,30 @@ class RecordTools(ToolBase):
|
|
|
11608
13140
|
},
|
|
11609
13141
|
)
|
|
11610
13142
|
|
|
13143
|
+
def _candidate_lookup_error(
|
|
13144
|
+
self,
|
|
13145
|
+
*,
|
|
13146
|
+
kind: str,
|
|
13147
|
+
field: FormField,
|
|
13148
|
+
value: JSONValue,
|
|
13149
|
+
error: QingflowApiError,
|
|
13150
|
+
) -> RecordInputError:
|
|
13151
|
+
"""Build the standard candidate lookup failure without raising it."""
|
|
13152
|
+
field_kind = "member" if kind == "member" else "department"
|
|
13153
|
+
return RecordInputError(
|
|
13154
|
+
message=f"{field_kind} candidates for field '{field.que_title}' could not be loaded",
|
|
13155
|
+
error_code=f"{kind.upper()}_CANDIDATE_LOOKUP_FAILED",
|
|
13156
|
+
fix_hint=(
|
|
13157
|
+
f"Run record_{field_kind}_candidates again after the backend error is resolved, "
|
|
13158
|
+
"then choose one returned item exactly."
|
|
13159
|
+
),
|
|
13160
|
+
details={
|
|
13161
|
+
"field": _field_ref_payload(field),
|
|
13162
|
+
"received_value": value,
|
|
13163
|
+
"candidate_error": error.to_dict(),
|
|
13164
|
+
},
|
|
13165
|
+
)
|
|
13166
|
+
|
|
11611
13167
|
def _candidate_keyword_from_value(
|
|
11612
13168
|
self,
|
|
11613
13169
|
value: JSONValue,
|
|
@@ -11872,14 +13428,7 @@ class RecordTools(ToolBase):
|
|
|
11872
13428
|
|
|
11873
13429
|
def _lookup_department_detail(self, context, keyword: str, *, purpose: str) -> JSONObject: # type: ignore[no-untyped-def]
|
|
11874
13430
|
"""执行内部辅助逻辑。"""
|
|
11875
|
-
|
|
11876
|
-
"GET",
|
|
11877
|
-
context,
|
|
11878
|
-
"/contact/deptByPage",
|
|
11879
|
-
params={"keyword": keyword, "pageNum": 1, "pageSize": 20},
|
|
11880
|
-
)
|
|
11881
|
-
rows = payload.get("list") if isinstance(payload, dict) else None
|
|
11882
|
-
items = [item for item in rows if isinstance(item, dict)] if isinstance(rows, list) else []
|
|
13431
|
+
items = self._search_workspace_departments(context, keyword=keyword)
|
|
11883
13432
|
normalized_keyword = keyword.strip()
|
|
11884
13433
|
exact = [
|
|
11885
13434
|
item for item in items
|
|
@@ -12404,6 +13953,8 @@ class RecordTools(ToolBase):
|
|
|
12404
13953
|
normalized_answers: list[JSONObject],
|
|
12405
13954
|
index: FieldIndex,
|
|
12406
13955
|
verify_list_type: int = DEFAULT_RECORD_LIST_TYPE,
|
|
13956
|
+
verify_role: int | None = None,
|
|
13957
|
+
verify_view_key: str | None = None,
|
|
12407
13958
|
) -> JSONObject:
|
|
12408
13959
|
"""执行内部辅助逻辑。"""
|
|
12409
13960
|
if apply_id is None:
|
|
@@ -12415,14 +13966,38 @@ class RecordTools(ToolBase):
|
|
|
12415
13966
|
"count_mismatches": [],
|
|
12416
13967
|
}
|
|
12417
13968
|
try:
|
|
12418
|
-
|
|
12419
|
-
|
|
12420
|
-
|
|
12421
|
-
|
|
12422
|
-
|
|
12423
|
-
|
|
13969
|
+
if verify_view_key:
|
|
13970
|
+
record = self.backend.request(
|
|
13971
|
+
"GET",
|
|
13972
|
+
context,
|
|
13973
|
+
f"/view/{verify_view_key}/apply/{apply_id}",
|
|
13974
|
+
)
|
|
13975
|
+
else:
|
|
13976
|
+
role = verify_role if verify_role is not None else 1
|
|
13977
|
+
record = self.backend.request(
|
|
13978
|
+
"GET",
|
|
13979
|
+
context,
|
|
13980
|
+
f"/app/{app_key}/apply/{apply_id}",
|
|
13981
|
+
params={"role": role, "listType": verify_list_type},
|
|
13982
|
+
)
|
|
12424
13983
|
except QingflowApiError as exc:
|
|
12425
|
-
if
|
|
13984
|
+
if verify_view_key:
|
|
13985
|
+
warning: JSONObject = {
|
|
13986
|
+
"code": "WRITE_VERIFY_CUSTOM_VIEW_READBACK_FAILED",
|
|
13987
|
+
"message": "Write was sent through a custom view route, but the same view could not be re-read for field-level verification.",
|
|
13988
|
+
}
|
|
13989
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
13990
|
+
return {
|
|
13991
|
+
"verified": False,
|
|
13992
|
+
"verification_mode": "custom_view_record_detail",
|
|
13993
|
+
"field_level_verified": False,
|
|
13994
|
+
"error": "custom_view_readback_failed",
|
|
13995
|
+
"missing_fields": [],
|
|
13996
|
+
"empty_fields": [],
|
|
13997
|
+
"count_mismatches": [],
|
|
13998
|
+
"warnings": [warning],
|
|
13999
|
+
}
|
|
14000
|
+
if not _is_record_permission_denied_error(exc):
|
|
12426
14001
|
raise
|
|
12427
14002
|
return self._verify_record_write_result_via_initiated_tasks(
|
|
12428
14003
|
context,
|
|
@@ -12485,7 +14060,7 @@ class RecordTools(ToolBase):
|
|
|
12485
14060
|
)
|
|
12486
14061
|
return {
|
|
12487
14062
|
"verified": not missing_fields and not empty_fields and not count_mismatches,
|
|
12488
|
-
"verification_mode": "initiated_record_view",
|
|
14063
|
+
"verification_mode": "custom_view_record_detail" if verify_view_key else "initiated_record_view",
|
|
12489
14064
|
"field_level_verified": True,
|
|
12490
14065
|
"missing_fields": missing_fields,
|
|
12491
14066
|
"empty_fields": empty_fields,
|
|
@@ -12716,6 +14291,8 @@ def _normalize_data_list_base_info_schema(payload: JSONValue) -> JSONObject:
|
|
|
12716
14291
|
if not isinstance(payload, dict):
|
|
12717
14292
|
return {}
|
|
12718
14293
|
que_base_infos = payload.get("queBaseInfos")
|
|
14294
|
+
if not isinstance(que_base_infos, list) and isinstance(payload.get("formQues"), list):
|
|
14295
|
+
que_base_infos = payload.get("formQues")
|
|
12719
14296
|
if not isinstance(que_base_infos, list):
|
|
12720
14297
|
return {}
|
|
12721
14298
|
return {
|
|
@@ -13102,6 +14679,44 @@ def _build_answer_backed_field_index(
|
|
|
13102
14679
|
)
|
|
13103
14680
|
|
|
13104
14681
|
|
|
14682
|
+
def _merge_subtable_parent_field(primary: FormField, extra: FormField) -> FormField:
|
|
14683
|
+
if primary.que_type not in SUBTABLE_QUE_TYPES or extra.que_type not in SUBTABLE_QUE_TYPES:
|
|
14684
|
+
return primary
|
|
14685
|
+
primary_raw = dict(primary.raw) if isinstance(primary.raw, dict) else {}
|
|
14686
|
+
extra_raw = dict(extra.raw) if isinstance(extra.raw, dict) else {}
|
|
14687
|
+
primary_subquestions = primary_raw.get("subQuestions")
|
|
14688
|
+
extra_subquestions = extra_raw.get("subQuestions")
|
|
14689
|
+
if not isinstance(primary_subquestions, list) or not isinstance(extra_subquestions, list):
|
|
14690
|
+
return primary
|
|
14691
|
+
merged_subquestions = [item for item in primary_subquestions if isinstance(item, dict)]
|
|
14692
|
+
seen_ids = {
|
|
14693
|
+
_coerce_count(item.get("queId"))
|
|
14694
|
+
for item in merged_subquestions
|
|
14695
|
+
if isinstance(item, dict) and _coerce_count(item.get("queId")) is not None
|
|
14696
|
+
}
|
|
14697
|
+
for item in extra_subquestions:
|
|
14698
|
+
if not isinstance(item, dict):
|
|
14699
|
+
continue
|
|
14700
|
+
que_id = _coerce_count(item.get("queId"))
|
|
14701
|
+
if que_id is not None and que_id in seen_ids:
|
|
14702
|
+
continue
|
|
14703
|
+
merged_subquestions.append(item)
|
|
14704
|
+
if que_id is not None:
|
|
14705
|
+
seen_ids.add(que_id)
|
|
14706
|
+
if len(merged_subquestions) == len(primary_subquestions):
|
|
14707
|
+
return primary
|
|
14708
|
+
merged_raw = dict(primary_raw)
|
|
14709
|
+
merged_raw["subQuestions"] = merged_subquestions
|
|
14710
|
+
merged_field = _clone_form_field(primary)
|
|
14711
|
+
merged_field.raw = merged_raw
|
|
14712
|
+
return merged_field
|
|
14713
|
+
|
|
14714
|
+
|
|
14715
|
+
def _replace_field_in_lookup(index: dict[str, list[FormField]], field: FormField) -> None:
|
|
14716
|
+
for key, fields in list(index.items()):
|
|
14717
|
+
index[key] = [field if existing.que_id == field.que_id else existing for existing in fields]
|
|
14718
|
+
|
|
14719
|
+
|
|
13105
14720
|
def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
|
|
13106
14721
|
by_id = dict(primary.by_id)
|
|
13107
14722
|
by_title = {key: list(value) for key, value in primary.by_title.items()}
|
|
@@ -13112,12 +14727,42 @@ def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
|
|
|
13112
14727
|
|
|
13113
14728
|
for field_id, field in extra.by_id.items():
|
|
13114
14729
|
if field_id in by_id:
|
|
14730
|
+
merged_field = _merge_subtable_parent_field(by_id[field_id], field)
|
|
14731
|
+
if merged_field is not by_id[field_id]:
|
|
14732
|
+
by_id[field_id] = merged_field
|
|
14733
|
+
_replace_field_in_lookup(by_title, merged_field)
|
|
14734
|
+
_replace_field_in_lookup(by_alias, merged_field)
|
|
13115
14735
|
continue
|
|
13116
14736
|
by_id[field_id] = field
|
|
13117
14737
|
by_title.setdefault(_normalize_field_lookup_key(field.que_title), []).append(field)
|
|
13118
14738
|
for alias in field.aliases:
|
|
13119
14739
|
by_alias.setdefault(_normalize_field_lookup_key(alias), []).append(field)
|
|
13120
14740
|
|
|
14741
|
+
for field_id, fields in extra.subtable_leaf_by_id.items():
|
|
14742
|
+
merged = subtable_leaf_by_id.setdefault(field_id, [])
|
|
14743
|
+
existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
|
|
14744
|
+
for field in fields:
|
|
14745
|
+
key = (field.field.que_id, field.parent_field.que_id)
|
|
14746
|
+
if key not in existing:
|
|
14747
|
+
merged.append(field)
|
|
14748
|
+
existing.add(key)
|
|
14749
|
+
for title, fields in extra.subtable_leaf_by_title.items():
|
|
14750
|
+
merged = subtable_leaf_by_title.setdefault(title, [])
|
|
14751
|
+
existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
|
|
14752
|
+
for field in fields:
|
|
14753
|
+
key = (field.field.que_id, field.parent_field.que_id)
|
|
14754
|
+
if key not in existing:
|
|
14755
|
+
merged.append(field)
|
|
14756
|
+
existing.add(key)
|
|
14757
|
+
for alias, fields in extra.subtable_leaf_by_alias.items():
|
|
14758
|
+
merged = subtable_leaf_by_alias.setdefault(alias, [])
|
|
14759
|
+
existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
|
|
14760
|
+
for field in fields:
|
|
14761
|
+
key = (field.field.que_id, field.parent_field.que_id)
|
|
14762
|
+
if key not in existing:
|
|
14763
|
+
merged.append(field)
|
|
14764
|
+
existing.add(key)
|
|
14765
|
+
|
|
13121
14766
|
return FieldIndex(
|
|
13122
14767
|
by_id=by_id,
|
|
13123
14768
|
by_title=by_title,
|
|
@@ -13397,6 +15042,13 @@ def _record_access_run_dir() -> Path:
|
|
|
13397
15042
|
return base_dir / run_id
|
|
13398
15043
|
|
|
13399
15044
|
|
|
15045
|
+
def _record_logs_run_dir() -> Path:
|
|
15046
|
+
custom_home = os.getenv("QINGFLOW_MCP_RECORD_LOGS_HOME")
|
|
15047
|
+
base_dir = Path(custom_home).expanduser() if custom_home else get_mcp_home() / "record-logs"
|
|
15048
|
+
run_id = f"{datetime.now(UTC).strftime('%Y%m%dT%H%M%SZ')}-{uuid4().hex[:8]}"
|
|
15049
|
+
return base_dir / run_id
|
|
15050
|
+
|
|
15051
|
+
|
|
13400
15052
|
def _record_access_field_payload(field: FormField) -> JSONObject:
|
|
13401
15053
|
return {
|
|
13402
15054
|
"field_id": field.que_id,
|
|
@@ -14165,6 +15817,159 @@ def _record_detail_log_unavailable_payload(source: str, reason: str) -> JSONObje
|
|
|
14165
15817
|
}
|
|
14166
15818
|
|
|
14167
15819
|
|
|
15820
|
+
def _record_logs_hidden_payload(source: str) -> JSONObject:
|
|
15821
|
+
return {
|
|
15822
|
+
"status": "hidden",
|
|
15823
|
+
"visible": False,
|
|
15824
|
+
"source": source,
|
|
15825
|
+
"complete": False,
|
|
15826
|
+
"items_count": 0,
|
|
15827
|
+
"pages_fetched": 0,
|
|
15828
|
+
"reported_total": None,
|
|
15829
|
+
"local_path": None,
|
|
15830
|
+
"preview_items": [],
|
|
15831
|
+
"warnings": [],
|
|
15832
|
+
}
|
|
15833
|
+
|
|
15834
|
+
|
|
15835
|
+
def _record_logs_unavailable_payload(source: str, reason: str) -> JSONObject:
|
|
15836
|
+
return {
|
|
15837
|
+
"status": "unavailable",
|
|
15838
|
+
"visible": None,
|
|
15839
|
+
"source": source,
|
|
15840
|
+
"reason": reason,
|
|
15841
|
+
"complete": False,
|
|
15842
|
+
"items_count": 0,
|
|
15843
|
+
"pages_fetched": 0,
|
|
15844
|
+
"reported_total": None,
|
|
15845
|
+
"local_path": None,
|
|
15846
|
+
"preview_items": [],
|
|
15847
|
+
"warnings": [],
|
|
15848
|
+
}
|
|
15849
|
+
|
|
15850
|
+
|
|
15851
|
+
def _record_logs_fetch_all_to_jsonl(
|
|
15852
|
+
*,
|
|
15853
|
+
fetch_page,
|
|
15854
|
+
normalizer,
|
|
15855
|
+
source: str,
|
|
15856
|
+
file_path: Path,
|
|
15857
|
+
deadline: float,
|
|
15858
|
+
) -> JSONObject: # type: ignore[no-untyped-def]
|
|
15859
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
15860
|
+
page_num = 1
|
|
15861
|
+
pages_fetched = 0
|
|
15862
|
+
items_count = 0
|
|
15863
|
+
reported_total: int | None = None
|
|
15864
|
+
preview_items: list[JSONObject] = []
|
|
15865
|
+
warnings: list[JSONObject] = []
|
|
15866
|
+
stopped_reason: str | None = None
|
|
15867
|
+
complete = True
|
|
15868
|
+
|
|
15869
|
+
with file_path.open("w", encoding="utf-8") as handle:
|
|
15870
|
+
while True:
|
|
15871
|
+
if _record_logs_time_budget_exceeded(deadline=deadline):
|
|
15872
|
+
complete = False
|
|
15873
|
+
stopped_reason = "time_budget_exceeded"
|
|
15874
|
+
warnings.append(_record_logs_time_budget_warning(source=source, pages_fetched=pages_fetched, items_count=items_count))
|
|
15875
|
+
break
|
|
15876
|
+
payload = fetch_page(page_num)
|
|
15877
|
+
pages_fetched += 1
|
|
15878
|
+
items = _record_detail_page_items(payload)
|
|
15879
|
+
if reported_total is None:
|
|
15880
|
+
reported_total = _record_detail_page_total(payload)
|
|
15881
|
+
if not items:
|
|
15882
|
+
break
|
|
15883
|
+
for item in items:
|
|
15884
|
+
normalized = normalizer(item)
|
|
15885
|
+
handle.write(json.dumps(normalized, ensure_ascii=False) + "\n")
|
|
15886
|
+
items_count += 1
|
|
15887
|
+
if len(preview_items) < RECORD_LOGS_PREVIEW_LIMIT:
|
|
15888
|
+
preview_items.append(normalized)
|
|
15889
|
+
if items_count >= RECORD_LOGS_MAX_ITEMS:
|
|
15890
|
+
complete = False
|
|
15891
|
+
stopped_reason = "item_limit_exceeded"
|
|
15892
|
+
warnings.append(_record_logs_item_limit_warning(source=source, item_limit=RECORD_LOGS_MAX_ITEMS))
|
|
15893
|
+
break
|
|
15894
|
+
if stopped_reason:
|
|
15895
|
+
break
|
|
15896
|
+
if reported_total is not None and items_count >= reported_total:
|
|
15897
|
+
break
|
|
15898
|
+
if reported_total is None and len(items) < RECORD_LOGS_PAGE_SIZE:
|
|
15899
|
+
break
|
|
15900
|
+
page_num += 1
|
|
15901
|
+
|
|
15902
|
+
return {
|
|
15903
|
+
"status": "ok" if complete else "partial",
|
|
15904
|
+
"visible": True,
|
|
15905
|
+
"source": source,
|
|
15906
|
+
"complete": complete,
|
|
15907
|
+
"items_count": items_count,
|
|
15908
|
+
"pages_fetched": pages_fetched,
|
|
15909
|
+
"page_size": RECORD_LOGS_PAGE_SIZE,
|
|
15910
|
+
"reported_total": reported_total,
|
|
15911
|
+
"local_path": str(file_path),
|
|
15912
|
+
"preview_items": preview_items,
|
|
15913
|
+
"warnings": warnings,
|
|
15914
|
+
"stopped_reason": stopped_reason,
|
|
15915
|
+
}
|
|
15916
|
+
|
|
15917
|
+
|
|
15918
|
+
def _record_logs_time_budget_exceeded(*, deadline: float) -> bool:
|
|
15919
|
+
return time.monotonic() + RECORD_LOGS_MIN_REMAINING_SECONDS >= deadline
|
|
15920
|
+
|
|
15921
|
+
|
|
15922
|
+
def _record_logs_time_budget_warning(*, source: str, pages_fetched: int, items_count: int) -> JSONObject:
|
|
15923
|
+
return {
|
|
15924
|
+
"code": "RECORD_LOGS_TIME_BUDGET_EXCEEDED",
|
|
15925
|
+
"source": source,
|
|
15926
|
+
"message": "record_logs_get stopped early to return partial JSONL files before the caller timeout.",
|
|
15927
|
+
"pages_fetched": pages_fetched,
|
|
15928
|
+
"items_count": items_count,
|
|
15929
|
+
}
|
|
15930
|
+
|
|
15931
|
+
|
|
15932
|
+
def _record_logs_item_limit_warning(*, source: str, item_limit: int) -> JSONObject:
|
|
15933
|
+
return {
|
|
15934
|
+
"code": "RECORD_LOGS_ITEM_LIMIT_EXCEEDED",
|
|
15935
|
+
"source": source,
|
|
15936
|
+
"message": f"record_logs_get stopped after the internal {item_limit} item limit.",
|
|
15937
|
+
"item_limit": item_limit,
|
|
15938
|
+
}
|
|
15939
|
+
|
|
15940
|
+
|
|
15941
|
+
def _record_logs_overall_status(*, data_logs: JSONObject, workflow_logs: JSONObject) -> str:
|
|
15942
|
+
statuses = {str(data_logs.get("status") or ""), str(workflow_logs.get("status") or "")}
|
|
15943
|
+
if statuses == {"unavailable"}:
|
|
15944
|
+
return "unavailable"
|
|
15945
|
+
if "partial" in statuses or "unavailable" in statuses:
|
|
15946
|
+
return "partial"
|
|
15947
|
+
return "success"
|
|
15948
|
+
|
|
15949
|
+
|
|
15950
|
+
def _record_logs_context_integrity(*, data_logs: JSONObject, workflow_logs: JSONObject) -> JSONObject:
|
|
15951
|
+
data_integrity = _record_logs_section_integrity(data_logs)
|
|
15952
|
+
workflow_integrity = _record_logs_section_integrity(workflow_logs)
|
|
15953
|
+
return {
|
|
15954
|
+
"data_logs": data_integrity,
|
|
15955
|
+
"workflow_logs": workflow_integrity,
|
|
15956
|
+
"safe_for_full_log_conclusion": data_integrity == "full" and workflow_integrity == "full",
|
|
15957
|
+
}
|
|
15958
|
+
|
|
15959
|
+
|
|
15960
|
+
def _record_logs_section_integrity(section: JSONObject) -> str:
|
|
15961
|
+
status = str(section.get("status") or "")
|
|
15962
|
+
if status == "ok" and section.get("complete") is True:
|
|
15963
|
+
return "full"
|
|
15964
|
+
if status == "hidden":
|
|
15965
|
+
return "hidden"
|
|
15966
|
+
if status == "partial":
|
|
15967
|
+
return "partial"
|
|
15968
|
+
if status == "unavailable":
|
|
15969
|
+
return "unavailable"
|
|
15970
|
+
return "unknown"
|
|
15971
|
+
|
|
15972
|
+
|
|
14168
15973
|
def _record_detail_log_page_payload(payload: JSONValue, *, normalizer, source: str) -> JSONObject: # type: ignore[no-untyped-def]
|
|
14169
15974
|
items = _record_detail_page_items(payload)
|
|
14170
15975
|
total = _record_detail_page_total(payload)
|
|
@@ -14348,6 +16153,28 @@ def _record_detail_unavailable_context(section: str, message: str, exc: Qingflow
|
|
|
14348
16153
|
"message": message,
|
|
14349
16154
|
"category": exc.category,
|
|
14350
16155
|
}
|
|
16156
|
+
if is_auth_like_error(exc):
|
|
16157
|
+
payload["auth_like"] = True
|
|
16158
|
+
payload["error_code"] = "AUTH_REQUIRED"
|
|
16159
|
+
if exc.backend_code is not None:
|
|
16160
|
+
payload["backend_code"] = exc.backend_code
|
|
16161
|
+
if exc.http_status is not None:
|
|
16162
|
+
payload["http_status"] = exc.http_status
|
|
16163
|
+
request_id = getattr(exc, "request_id", None)
|
|
16164
|
+
if request_id:
|
|
16165
|
+
payload["request_id"] = request_id
|
|
16166
|
+
details = exc.details if isinstance(exc.details, dict) else {}
|
|
16167
|
+
error_code = details.get("error_code")
|
|
16168
|
+
if error_code and not payload.get("error_code"):
|
|
16169
|
+
payload["error_code"] = error_code
|
|
16170
|
+
return payload
|
|
16171
|
+
|
|
16172
|
+
|
|
16173
|
+
def _record_detail_error_warning_fields(exc: QingflowApiError) -> JSONObject:
|
|
16174
|
+
payload: JSONObject = {"category": exc.category}
|
|
16175
|
+
if is_auth_like_error(exc):
|
|
16176
|
+
payload["auth_like"] = True
|
|
16177
|
+
payload["error_code"] = "AUTH_REQUIRED"
|
|
14351
16178
|
if exc.backend_code is not None:
|
|
14352
16179
|
payload["backend_code"] = exc.backend_code
|
|
14353
16180
|
if exc.http_status is not None:
|
|
@@ -14357,11 +16184,35 @@ def _record_detail_unavailable_context(section: str, message: str, exc: Qingflow
|
|
|
14357
16184
|
payload["request_id"] = request_id
|
|
14358
16185
|
details = exc.details if isinstance(exc.details, dict) else {}
|
|
14359
16186
|
error_code = details.get("error_code")
|
|
14360
|
-
if error_code:
|
|
16187
|
+
if error_code and not payload.get("error_code"):
|
|
14361
16188
|
payload["error_code"] = error_code
|
|
14362
16189
|
return payload
|
|
14363
16190
|
|
|
14364
16191
|
|
|
16192
|
+
def _record_detail_refreshed_source_url(refresh_result: Any) -> str | None:
|
|
16193
|
+
if isinstance(refresh_result, dict):
|
|
16194
|
+
return _normalize_optional_text(refresh_result.get("source_url"))
|
|
16195
|
+
return _normalize_optional_text(refresh_result)
|
|
16196
|
+
|
|
16197
|
+
|
|
16198
|
+
def _record_detail_append_refresh_warning(
|
|
16199
|
+
warnings: list[JSONObject],
|
|
16200
|
+
refresh_result: Any,
|
|
16201
|
+
*,
|
|
16202
|
+
id_key: str,
|
|
16203
|
+
id_value: str,
|
|
16204
|
+
) -> None:
|
|
16205
|
+
if not isinstance(refresh_result, dict):
|
|
16206
|
+
return
|
|
16207
|
+
warning = refresh_result.get("warning")
|
|
16208
|
+
if not isinstance(warning, dict):
|
|
16209
|
+
return
|
|
16210
|
+
payload: JSONObject = dict(warning)
|
|
16211
|
+
payload.setdefault("code", "ASSET_STORAGE_URL_REFRESH_FAILED")
|
|
16212
|
+
payload.setdefault(id_key, id_value)
|
|
16213
|
+
warnings.append(payload)
|
|
16214
|
+
|
|
16215
|
+
|
|
14365
16216
|
_RECORD_MEDIA_IMG_SRC_RE = re.compile(r"""<img\b[^>]*\bsrc\s*=\s*["']?([^"'\s>]+)""", re.IGNORECASE)
|
|
14366
16217
|
_RECORD_MEDIA_MD_IMAGE_RE = re.compile(r"""!\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)""")
|
|
14367
16218
|
_RECORD_MEDIA_URL_RE = re.compile(r"""https?://[^\s<>"')\]]+""", re.IGNORECASE)
|
|
@@ -14516,7 +16367,14 @@ def _record_detail_media_assets_payload(
|
|
|
14516
16367
|
except QingflowApiError as exc:
|
|
14517
16368
|
blocked = exc.http_status in {401, 403}
|
|
14518
16369
|
if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
|
|
14519
|
-
|
|
16370
|
+
refresh_result = refresh_source_url(candidate)
|
|
16371
|
+
_record_detail_append_refresh_warning(
|
|
16372
|
+
warnings,
|
|
16373
|
+
refresh_result,
|
|
16374
|
+
id_key="asset_id",
|
|
16375
|
+
id_value=asset_id,
|
|
16376
|
+
)
|
|
16377
|
+
refreshed_url = _record_detail_refreshed_source_url(refresh_result)
|
|
14520
16378
|
if refreshed_url and refreshed_url != source_url:
|
|
14521
16379
|
refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
|
|
14522
16380
|
try:
|
|
@@ -14558,14 +16416,13 @@ def _record_detail_media_assets_payload(
|
|
|
14558
16416
|
"readable_by_agent": False,
|
|
14559
16417
|
}
|
|
14560
16418
|
)
|
|
14561
|
-
|
|
14562
|
-
|
|
14563
|
-
|
|
14564
|
-
|
|
14565
|
-
|
|
14566
|
-
|
|
14567
|
-
|
|
14568
|
-
)
|
|
16419
|
+
warning: JSONObject = {
|
|
16420
|
+
"code": warning_code,
|
|
16421
|
+
"asset_id": asset_id,
|
|
16422
|
+
"message": f"record_get could not download image asset {asset_id}: {exc.message}",
|
|
16423
|
+
}
|
|
16424
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
16425
|
+
warnings.append(warning)
|
|
14569
16426
|
continue
|
|
14570
16427
|
|
|
14571
16428
|
if not isinstance(content, bytes):
|
|
@@ -14783,7 +16640,14 @@ def _record_detail_file_assets_payload(
|
|
|
14783
16640
|
except QingflowApiError as exc:
|
|
14784
16641
|
blocked = exc.http_status in {401, 403}
|
|
14785
16642
|
if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
|
|
14786
|
-
|
|
16643
|
+
refresh_result = refresh_source_url(candidate)
|
|
16644
|
+
_record_detail_append_refresh_warning(
|
|
16645
|
+
warnings,
|
|
16646
|
+
refresh_result,
|
|
16647
|
+
id_key="file_asset_id",
|
|
16648
|
+
id_value=file_asset_id,
|
|
16649
|
+
)
|
|
16650
|
+
refreshed_url = _record_detail_refreshed_source_url(refresh_result)
|
|
14787
16651
|
if refreshed_url and refreshed_url != source_url:
|
|
14788
16652
|
refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
|
|
14789
16653
|
try:
|
|
@@ -14830,14 +16694,13 @@ def _record_detail_file_assets_payload(
|
|
|
14830
16694
|
"extraction": {"status": "failed", "text_path": None, "preview": None},
|
|
14831
16695
|
}
|
|
14832
16696
|
)
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
)
|
|
16697
|
+
warning = {
|
|
16698
|
+
"code": warning_code,
|
|
16699
|
+
"file_asset_id": file_asset_id,
|
|
16700
|
+
"message": f"record_get could not download file asset {file_asset_id}: {exc.message}",
|
|
16701
|
+
}
|
|
16702
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
16703
|
+
warnings.append(warning)
|
|
14841
16704
|
continue
|
|
14842
16705
|
|
|
14843
16706
|
if not isinstance(content, bytes):
|