@qingflow-tech/qingflow-app-builder-mcp 1.0.11 → 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/service.py +1488 -288
- package/src/qingflow_mcp/cli/commands/builder.py +2 -2
- 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 +39 -11
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +206 -7
- package/src/qingflow_mcp/cli/main.py +47 -3
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +21 -15
- package/src/qingflow_mcp/response_trim.py +68 -13
- package/src/qingflow_mcp/server.py +11 -9
- package/src/qingflow_mcp/server_app_builder.py +3 -2
- package/src/qingflow_mcp/server_app_user.py +15 -13
- package/src/qingflow_mcp/solution/executor.py +112 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +36 -11
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- 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 +118 -6
- 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 +1042 -338
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -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 (
|
|
@@ -131,6 +131,27 @@ SCHEMA_LINKAGE_REFERENCE_SOURCE_MESSAGE = "updating this field may auto-fill or
|
|
|
131
131
|
SCHEMA_LINKAGE_REFERENCE_TARGET_MESSAGE = "this field is usually filled from an upstream reference selection or default matching logic"
|
|
132
132
|
SCHEMA_LINKAGE_REFERENCE_BOTH_MESSAGE = "this field participates in reference-driven auto-fill logic"
|
|
133
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
|
|
134
155
|
|
|
135
156
|
|
|
136
157
|
@dataclass(slots=True)
|
|
@@ -215,6 +236,13 @@ class AccessibleViewRoute:
|
|
|
215
236
|
view_type: str | None = None
|
|
216
237
|
|
|
217
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
|
+
|
|
218
246
|
@dataclass(slots=True)
|
|
219
247
|
class RecordContextRouteProbe:
|
|
220
248
|
route: AccessibleViewRoute
|
|
@@ -471,7 +499,7 @@ class RecordTools(ToolBase):
|
|
|
471
499
|
output_profile=output_profile,
|
|
472
500
|
)
|
|
473
501
|
|
|
474
|
-
@mcp.tool(description="Read all visible data logs and workflow logs for one Qingflow record into local JSONL files. This tool hides pagination and returns file paths plus completeness metadata.")
|
|
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.")
|
|
475
503
|
def record_logs_get(
|
|
476
504
|
profile: str = DEFAULT_PROFILE,
|
|
477
505
|
app_key: str = "",
|
|
@@ -502,12 +530,14 @@ class RecordTools(ToolBase):
|
|
|
502
530
|
def record_update_schema_get(
|
|
503
531
|
app_key: str = "",
|
|
504
532
|
record_id: str = "",
|
|
533
|
+
view_id: str | None = None,
|
|
505
534
|
output_profile: str = "normal",
|
|
506
535
|
) -> JSONObject:
|
|
507
536
|
return self.record_update_schema_get_public(
|
|
508
537
|
profile=DEFAULT_PROFILE,
|
|
509
538
|
app_key=app_key,
|
|
510
539
|
record_id=record_id,
|
|
540
|
+
view_id=view_id,
|
|
511
541
|
output_profile=output_profile,
|
|
512
542
|
)
|
|
513
543
|
|
|
@@ -537,6 +567,7 @@ class RecordTools(ToolBase):
|
|
|
537
567
|
description=(
|
|
538
568
|
"Update one Qingflow record using a field map. "
|
|
539
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. "
|
|
540
571
|
"It first tries the data-manager direct route, then falls back to the frontend custom-view edit route when available. "
|
|
541
572
|
"Use record_update_schema_get for diagnostics or complex field-scope inspection."
|
|
542
573
|
)
|
|
@@ -546,6 +577,7 @@ class RecordTools(ToolBase):
|
|
|
546
577
|
record_id: str | None = None,
|
|
547
578
|
fields: JSONObject | None = None,
|
|
548
579
|
items: list[JSONObject] | None = None,
|
|
580
|
+
view_id: str | None = None,
|
|
549
581
|
dry_run: bool = False,
|
|
550
582
|
verify_write: bool = True,
|
|
551
583
|
output_profile: str = "normal",
|
|
@@ -556,6 +588,7 @@ class RecordTools(ToolBase):
|
|
|
556
588
|
record_id=record_id,
|
|
557
589
|
fields=fields,
|
|
558
590
|
items=items,
|
|
591
|
+
view_id=view_id,
|
|
559
592
|
dry_run=dry_run,
|
|
560
593
|
verify_write=verify_write,
|
|
561
594
|
output_profile=output_profile,
|
|
@@ -564,13 +597,14 @@ class RecordTools(ToolBase):
|
|
|
564
597
|
@mcp.tool(
|
|
565
598
|
description=(
|
|
566
599
|
"Delete Qingflow records by record_id or record_ids. "
|
|
567
|
-
"
|
|
600
|
+
"Pass view_id when deleting from a known system list route; custom view deletion is not supported by this backend route."
|
|
568
601
|
)
|
|
569
602
|
)
|
|
570
603
|
def record_delete(
|
|
571
604
|
app_key: str = "",
|
|
572
605
|
record_id: str | None = None,
|
|
573
606
|
record_ids: list[str] | None = None,
|
|
607
|
+
view_id: str | None = None,
|
|
574
608
|
output_profile: str = "normal",
|
|
575
609
|
) -> JSONObject:
|
|
576
610
|
return self.record_delete_public(
|
|
@@ -578,6 +612,7 @@ class RecordTools(ToolBase):
|
|
|
578
612
|
app_key=app_key,
|
|
579
613
|
record_id=record_id,
|
|
580
614
|
record_ids=record_ids or [],
|
|
615
|
+
view_id=view_id,
|
|
581
616
|
output_profile=output_profile,
|
|
582
617
|
)
|
|
583
618
|
|
|
@@ -849,6 +884,7 @@ class RecordTools(ToolBase):
|
|
|
849
884
|
profile: str = DEFAULT_PROFILE,
|
|
850
885
|
app_key: str,
|
|
851
886
|
record_id: Any,
|
|
887
|
+
view_id: str | None = None,
|
|
852
888
|
output_profile: str = "normal",
|
|
853
889
|
) -> JSONObject:
|
|
854
890
|
"""执行记录相关逻辑。"""
|
|
@@ -860,21 +896,44 @@ class RecordTools(ToolBase):
|
|
|
860
896
|
def runner(session_profile, context):
|
|
861
897
|
request_route = self._request_route_payload(context)
|
|
862
898
|
self._clear_record_schema_caches(profile=profile, app_key=app_key, clear_view_caches=True)
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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)
|
|
877
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]
|
|
878
937
|
probes = self._probe_candidate_record_contexts(
|
|
879
938
|
context,
|
|
880
939
|
app_key=app_key,
|
|
@@ -976,6 +1035,7 @@ class RecordTools(ToolBase):
|
|
|
976
1035
|
output_profile=normalized_output_profile,
|
|
977
1036
|
view_probe_summary=probe_summary,
|
|
978
1037
|
ambiguous_fields=[],
|
|
1038
|
+
preferred_view_id=preferred_view_id,
|
|
979
1039
|
)
|
|
980
1040
|
|
|
981
1041
|
ambiguous_field_ids: set[int] = set()
|
|
@@ -1022,6 +1082,7 @@ class RecordTools(ToolBase):
|
|
|
1022
1082
|
output_profile=normalized_output_profile,
|
|
1023
1083
|
view_probe_summary=probe_summary,
|
|
1024
1084
|
ambiguous_fields=ambiguous_fields,
|
|
1085
|
+
preferred_view_id=preferred_view_id,
|
|
1025
1086
|
)
|
|
1026
1087
|
|
|
1027
1088
|
response: JSONObject = {
|
|
@@ -1046,6 +1107,8 @@ class RecordTools(ToolBase):
|
|
|
1046
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.",
|
|
1047
1108
|
},
|
|
1048
1109
|
}
|
|
1110
|
+
if preferred_view_id:
|
|
1111
|
+
response["preferred_view_id"] = preferred_view_id
|
|
1049
1112
|
if normalized_output_profile == "verbose":
|
|
1050
1113
|
response["view_probe_summary"] = probe_summary
|
|
1051
1114
|
response["record_context_probe"] = probe_summary
|
|
@@ -1106,6 +1169,7 @@ class RecordTools(ToolBase):
|
|
|
1106
1169
|
output_profile: str,
|
|
1107
1170
|
view_probe_summary: list[JSONObject],
|
|
1108
1171
|
ambiguous_fields: list[JSONObject],
|
|
1172
|
+
preferred_view_id: str | None = None,
|
|
1109
1173
|
) -> JSONObject:
|
|
1110
1174
|
"""执行内部辅助逻辑。"""
|
|
1111
1175
|
response: JSONObject = {
|
|
@@ -1123,6 +1187,8 @@ class RecordTools(ToolBase):
|
|
|
1123
1187
|
"payload_template": {},
|
|
1124
1188
|
"recommended_next_actions": recommended_next_actions,
|
|
1125
1189
|
}
|
|
1190
|
+
if preferred_view_id:
|
|
1191
|
+
response["preferred_view_id"] = preferred_view_id
|
|
1126
1192
|
if output_profile == "verbose":
|
|
1127
1193
|
response["view_probe_summary"] = view_probe_summary
|
|
1128
1194
|
response["ambiguous_fields"] = ambiguous_fields
|
|
@@ -1440,24 +1506,58 @@ class RecordTools(ToolBase):
|
|
|
1440
1506
|
)
|
|
1441
1507
|
warnings: list[JSONObject] = []
|
|
1442
1508
|
scope_source = "static_applicant_scope"
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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,
|
|
1447
1555
|
app_key=app_key,
|
|
1448
|
-
|
|
1556
|
+
record_id_text=record_id_text,
|
|
1449
1557
|
workflow_node_id=workflow_node_id,
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
scope_source = "backend_runtime_scope"
|
|
1454
|
-
else:
|
|
1455
|
-
items = self._resolve_member_candidates(context, field, keyword=keyword)
|
|
1456
|
-
warnings.append(
|
|
1457
|
-
{
|
|
1458
|
-
"code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
|
|
1459
|
-
"message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
|
|
1460
|
-
}
|
|
1558
|
+
fields_present=bool(normalized_fields),
|
|
1559
|
+
keyword=keyword,
|
|
1560
|
+
scope_source=scope_source,
|
|
1461
1561
|
)
|
|
1462
1562
|
total = len(items)
|
|
1463
1563
|
start = (page_num - 1) * page_size
|
|
@@ -1550,41 +1650,75 @@ class RecordTools(ToolBase):
|
|
|
1550
1650
|
)
|
|
1551
1651
|
warnings: list[JSONObject] = []
|
|
1552
1652
|
scope_source = "static_applicant_scope"
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
profile,
|
|
1556
|
-
context,
|
|
1557
|
-
app_key=app_key,
|
|
1558
|
-
record_id=record_id_int,
|
|
1559
|
-
workflow_node_id=workflow_node_id,
|
|
1560
|
-
fields=normalized_fields,
|
|
1561
|
-
)
|
|
1562
|
-
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1563
|
-
scope_source = "backend_runtime_scope"
|
|
1564
|
-
else:
|
|
1565
|
-
items = self._resolve_department_candidates(context, field, keyword=keyword)
|
|
1566
|
-
scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
|
|
1567
|
-
if (
|
|
1568
|
-
not items
|
|
1569
|
-
and field.dept_select_scope_type == 2
|
|
1570
|
-
and not _scope_has_dynamic_or_external(scope)
|
|
1571
|
-
and not list(scope.get("depart") or [])
|
|
1572
|
-
):
|
|
1653
|
+
try:
|
|
1654
|
+
if runtime_lookup:
|
|
1573
1655
|
state = self._build_candidate_lookup_state(
|
|
1574
1656
|
profile,
|
|
1575
1657
|
context,
|
|
1576
1658
|
app_key=app_key,
|
|
1577
|
-
record_id=
|
|
1578
|
-
workflow_node_id=
|
|
1579
|
-
fields=
|
|
1659
|
+
record_id=record_id_int,
|
|
1660
|
+
workflow_node_id=workflow_node_id,
|
|
1661
|
+
fields=normalized_fields,
|
|
1580
1662
|
)
|
|
1581
1663
|
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1582
1664
|
scope_source = "backend_runtime_scope"
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
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,
|
|
1588
1722
|
)
|
|
1589
1723
|
total = len(items)
|
|
1590
1724
|
start = (page_num - 1) * page_size
|
|
@@ -1646,6 +1780,21 @@ class RecordTools(ToolBase):
|
|
|
1646
1780
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
1647
1781
|
if limit <= 0:
|
|
1648
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
|
+
)
|
|
1649
1798
|
legacy_warnings = _detect_analyze_legacy_warnings(
|
|
1650
1799
|
dimensions=dimensions,
|
|
1651
1800
|
metrics=metrics,
|
|
@@ -1662,7 +1811,7 @@ class RecordTools(ToolBase):
|
|
|
1662
1811
|
list_type=list_type,
|
|
1663
1812
|
view_key=view_key,
|
|
1664
1813
|
view_name=view_name,
|
|
1665
|
-
allow_default=
|
|
1814
|
+
allow_default=False,
|
|
1666
1815
|
)
|
|
1667
1816
|
if not _view_type_supports_analysis(resolved_view.view_type):
|
|
1668
1817
|
raise_tool_error(
|
|
@@ -1743,6 +1892,21 @@ class RecordTools(ToolBase):
|
|
|
1743
1892
|
raise_tool_error(QingflowApiError.config_error("limit must be positive"))
|
|
1744
1893
|
if page <= 0:
|
|
1745
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
|
+
)
|
|
1746
1910
|
view_route, compatibility_warnings = self._resolve_accessible_view_route_for_public(
|
|
1747
1911
|
profile=profile,
|
|
1748
1912
|
app_key=app_key,
|
|
@@ -1750,7 +1914,7 @@ class RecordTools(ToolBase):
|
|
|
1750
1914
|
list_type=list_type,
|
|
1751
1915
|
view_key=view_key,
|
|
1752
1916
|
view_name=view_name,
|
|
1753
|
-
allow_default=
|
|
1917
|
+
allow_default=False,
|
|
1754
1918
|
)
|
|
1755
1919
|
if not _view_type_supports_analysis(view_route.view_type):
|
|
1756
1920
|
raise_tool_error(
|
|
@@ -2224,6 +2388,7 @@ class RecordTools(ToolBase):
|
|
|
2224
2388
|
requested_output_profile = _normalize_record_get_detail_output_profile(output_profile)
|
|
2225
2389
|
record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
|
|
2226
2390
|
normalized_columns = _normalize_public_column_selectors(columns)
|
|
2391
|
+
explicit_view_id = _normalize_optional_text(view_id)
|
|
2227
2392
|
|
|
2228
2393
|
def runner(session_profile, context):
|
|
2229
2394
|
resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
|
|
@@ -2249,17 +2414,45 @@ class RecordTools(ToolBase):
|
|
|
2249
2414
|
"code": "OUTPUT_PROFILE_DEPRECATED_FOR_DETAIL_CONTEXT",
|
|
2250
2415
|
"message": f"output_profile={requested_output_profile!r} is deprecated for record_get; detail_context is always returned.",
|
|
2251
2416
|
})
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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
|
|
2263
2456
|
|
|
2264
2457
|
return self._run_record_tool(profile, runner)
|
|
2265
2458
|
|
|
@@ -2274,6 +2467,16 @@ class RecordTools(ToolBase):
|
|
|
2274
2467
|
) -> JSONObject:
|
|
2275
2468
|
"""读取单条记录可见的全量数据日志和流程日志,写入本地 JSONL。"""
|
|
2276
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
|
+
)
|
|
2479
|
+
)
|
|
2277
2480
|
|
|
2278
2481
|
def runner(session_profile, context):
|
|
2279
2482
|
resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
|
|
@@ -2284,21 +2487,45 @@ class RecordTools(ToolBase):
|
|
|
2284
2487
|
list_type=None,
|
|
2285
2488
|
view_key=None,
|
|
2286
2489
|
view_name=None,
|
|
2287
|
-
allow_default=
|
|
2490
|
+
allow_default=False,
|
|
2288
2491
|
)
|
|
2289
2492
|
warnings: list[JSONObject] = []
|
|
2290
2493
|
warnings.extend(compatibility_warnings)
|
|
2291
2494
|
warnings.extend(_view_filter_trust_warnings(resolved_view))
|
|
2292
2495
|
unavailable_context: list[JSONObject] = []
|
|
2293
2496
|
|
|
2294
|
-
schema =
|
|
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
|
+
)
|
|
2295
2510
|
index = _build_top_level_field_index(schema)
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
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
|
+
)
|
|
2302
2529
|
audit_context = _record_detail_audit_context(audit_info, workflow_node_id=None)
|
|
2303
2530
|
detail_result, used_list_type, used_role = self._record_get_apply_detail(
|
|
2304
2531
|
context,
|
|
@@ -2308,6 +2535,17 @@ class RecordTools(ToolBase):
|
|
|
2308
2535
|
audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
|
|
2309
2536
|
)
|
|
2310
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
|
+
)
|
|
2311
2549
|
selected_fields = list(index.by_id.values())
|
|
2312
2550
|
fields = [
|
|
2313
2551
|
_record_detail_field_payload(field, _find_answer_for_field(cast(list[JSONValue], answer_list), field), focus_id_set=set())
|
|
@@ -2401,14 +2639,41 @@ class RecordTools(ToolBase):
|
|
|
2401
2639
|
warnings: list[JSONObject],
|
|
2402
2640
|
) -> JSONObject:
|
|
2403
2641
|
"""执行内部辅助逻辑。"""
|
|
2404
|
-
|
|
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
|
+
)
|
|
2405
2658
|
index = _build_top_level_field_index(schema)
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
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
|
+
)
|
|
2412
2677
|
audit_context = _record_detail_audit_context(audit_info, workflow_node_id=workflow_node_id)
|
|
2413
2678
|
detail_result, used_list_type, used_role = self._record_get_apply_detail(
|
|
2414
2679
|
context,
|
|
@@ -2418,13 +2683,24 @@ class RecordTools(ToolBase):
|
|
|
2418
2683
|
audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
|
|
2419
2684
|
)
|
|
2420
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
|
+
)
|
|
2421
2697
|
selected_fields = list(index.by_id.values())
|
|
2422
2698
|
row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id_int)
|
|
2423
2699
|
normalized_record, _normalized_ambiguous_fields = _build_normalized_row_from_answers(
|
|
2424
2700
|
cast(list[JSONValue], answer_list),
|
|
2425
2701
|
selected_fields,
|
|
2426
2702
|
)
|
|
2427
|
-
if self._record_get_needs_schema_refresh(
|
|
2703
|
+
if schema_available and self._record_get_needs_schema_refresh(
|
|
2428
2704
|
answer_list=cast(list[JSONValue], answer_list),
|
|
2429
2705
|
selected_fields=selected_fields,
|
|
2430
2706
|
record=row,
|
|
@@ -2440,7 +2716,6 @@ class RecordTools(ToolBase):
|
|
|
2440
2716
|
index = _build_top_level_field_index(schema)
|
|
2441
2717
|
selected_fields = list(index.by_id.values())
|
|
2442
2718
|
|
|
2443
|
-
unavailable_context: list[JSONObject] = []
|
|
2444
2719
|
dynamic_reference_answers, dynamic_reference_unavailable = self._record_get_dynamic_reference_answers(
|
|
2445
2720
|
context,
|
|
2446
2721
|
app_key=app_key,
|
|
@@ -2599,7 +2874,20 @@ class RecordTools(ToolBase):
|
|
|
2599
2874
|
) -> JSONObject:
|
|
2600
2875
|
"""执行内部辅助逻辑。"""
|
|
2601
2876
|
if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
2602
|
-
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
|
+
)
|
|
2603
2891
|
return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
|
|
2604
2892
|
|
|
2605
2893
|
def _record_get_audit_info(
|
|
@@ -2660,7 +2948,7 @@ class RecordTools(ToolBase):
|
|
|
2660
2948
|
)
|
|
2661
2949
|
return result if isinstance(result, dict) else {"value": result}, list_type, role
|
|
2662
2950
|
except QingflowApiError as exc:
|
|
2663
|
-
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):
|
|
2664
2952
|
raise
|
|
2665
2953
|
last_error: QingflowApiError = exc
|
|
2666
2954
|
for fallback_list_type in (14, 1, 2, 12):
|
|
@@ -2678,7 +2966,7 @@ class RecordTools(ToolBase):
|
|
|
2678
2966
|
return result if isinstance(result, dict) else {"value": result}, fallback_list_type, role
|
|
2679
2967
|
except QingflowApiError as fallback_exc:
|
|
2680
2968
|
last_error = fallback_exc
|
|
2681
|
-
if fallback_exc
|
|
2969
|
+
if _is_record_permission_denied_error(fallback_exc):
|
|
2682
2970
|
continue
|
|
2683
2971
|
raise
|
|
2684
2972
|
raise last_error
|
|
@@ -2776,6 +3064,8 @@ class RecordTools(ToolBase):
|
|
|
2776
3064
|
if target_app_key == app_key and str(target_record_id) == str(source_record_id):
|
|
2777
3065
|
reference_payload["self_reference"] = True
|
|
2778
3066
|
except QingflowApiError as exc:
|
|
3067
|
+
if is_auth_like_error(exc):
|
|
3068
|
+
raise
|
|
2779
3069
|
unavailable = _record_detail_unavailable_context(
|
|
2780
3070
|
"reference_detail",
|
|
2781
3071
|
f"引用字段「{field.que_title}」的目标记录详情获取失败。",
|
|
@@ -2873,6 +3163,8 @@ class RecordTools(ToolBase):
|
|
|
2873
3163
|
json_body=body,
|
|
2874
3164
|
)
|
|
2875
3165
|
except QingflowApiError as exc:
|
|
3166
|
+
if is_auth_like_error(exc):
|
|
3167
|
+
raise
|
|
2876
3168
|
unavailable = _record_detail_unavailable_context(
|
|
2877
3169
|
"reference_runtime_match",
|
|
2878
3170
|
"动态引用字段匹配数据获取失败。",
|
|
@@ -2927,6 +3219,8 @@ class RecordTools(ToolBase):
|
|
|
2927
3219
|
},
|
|
2928
3220
|
)
|
|
2929
3221
|
except QingflowApiError as exc:
|
|
3222
|
+
if is_auth_like_error(exc):
|
|
3223
|
+
raise
|
|
2930
3224
|
unavailable_context.append(_record_detail_unavailable_context("log_visibility", "侧边栏日志可见性获取失败。", exc))
|
|
2931
3225
|
return {"status": "unavailable", "channel": channel, "data_log_visible": None, "workflow_log_visible": None}
|
|
2932
3226
|
payload = visibility if isinstance(visibility, dict) else {}
|
|
@@ -2980,6 +3274,8 @@ class RecordTools(ToolBase):
|
|
|
2980
3274
|
source="data_logs",
|
|
2981
3275
|
)
|
|
2982
3276
|
except QingflowApiError as exc:
|
|
3277
|
+
if is_auth_like_error(exc):
|
|
3278
|
+
raise
|
|
2983
3279
|
unavailable_context.append(_record_detail_unavailable_context("data_logs", "最近数据日志获取失败。", exc))
|
|
2984
3280
|
return _record_detail_log_unavailable_payload("data_logs", "fetch_unavailable")
|
|
2985
3281
|
|
|
@@ -3033,6 +3329,8 @@ class RecordTools(ToolBase):
|
|
|
3033
3329
|
source="workflow_logs",
|
|
3034
3330
|
)
|
|
3035
3331
|
except QingflowApiError as exc:
|
|
3332
|
+
if is_auth_like_error(exc):
|
|
3333
|
+
raise
|
|
3036
3334
|
unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "流程日志本次获取失败。", exc))
|
|
3037
3335
|
return _record_detail_log_unavailable_payload("workflow_logs", "fetch_unavailable")
|
|
3038
3336
|
|
|
@@ -3076,6 +3374,8 @@ class RecordTools(ToolBase):
|
|
|
3076
3374
|
deadline=deadline,
|
|
3077
3375
|
)
|
|
3078
3376
|
except QingflowApiError as exc:
|
|
3377
|
+
if is_auth_like_error(exc):
|
|
3378
|
+
raise
|
|
3079
3379
|
unavailable_context.append(_record_detail_unavailable_context("data_logs", "全量数据日志获取失败。", exc))
|
|
3080
3380
|
return _record_logs_unavailable_payload("data_logs", "fetch_unavailable")
|
|
3081
3381
|
|
|
@@ -3135,6 +3435,8 @@ class RecordTools(ToolBase):
|
|
|
3135
3435
|
deadline=deadline,
|
|
3136
3436
|
)
|
|
3137
3437
|
except QingflowApiError as exc:
|
|
3438
|
+
if is_auth_like_error(exc):
|
|
3439
|
+
raise
|
|
3138
3440
|
unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "全量流程日志获取失败。", exc))
|
|
3139
3441
|
return _record_logs_unavailable_payload("workflow_logs", "fetch_unavailable")
|
|
3140
3442
|
|
|
@@ -3167,6 +3469,8 @@ class RecordTools(ToolBase):
|
|
|
3167
3469
|
params["auditNodeId"] = audit_node_id
|
|
3168
3470
|
payload = self.backend.request("GET", context, f"/app/{app_key}/asosChart", params=params)
|
|
3169
3471
|
except QingflowApiError as exc:
|
|
3472
|
+
if is_auth_like_error(exc):
|
|
3473
|
+
raise
|
|
3170
3474
|
unavailable_context.append(_record_detail_unavailable_context("associated_resources", "关联资源获取失败。", exc))
|
|
3171
3475
|
return []
|
|
3172
3476
|
return [_record_detail_associated_resource(item) for item in _record_detail_associated_resource_items(payload)]
|
|
@@ -3204,16 +3508,17 @@ class RecordTools(ToolBase):
|
|
|
3204
3508
|
refresh_source_url=refresh_source_url,
|
|
3205
3509
|
)
|
|
3206
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))
|
|
3207
3517
|
return {
|
|
3208
3518
|
"status": "unavailable",
|
|
3209
3519
|
"local_dir": None,
|
|
3210
3520
|
"items": [],
|
|
3211
|
-
"warnings": [
|
|
3212
|
-
{
|
|
3213
|
-
"code": "MEDIA_ASSETS_UNAVAILABLE",
|
|
3214
|
-
"message": f"record_get could not collect media assets: {exc}",
|
|
3215
|
-
}
|
|
3216
|
-
],
|
|
3521
|
+
"warnings": [warning],
|
|
3217
3522
|
}
|
|
3218
3523
|
|
|
3219
3524
|
def _record_get_file_assets(
|
|
@@ -3251,16 +3556,17 @@ class RecordTools(ToolBase):
|
|
|
3251
3556
|
refresh_source_url=refresh_source_url,
|
|
3252
3557
|
)
|
|
3253
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))
|
|
3254
3565
|
return {
|
|
3255
3566
|
"status": "unavailable",
|
|
3256
3567
|
"local_dir": None,
|
|
3257
3568
|
"items": [],
|
|
3258
|
-
"warnings": [
|
|
3259
|
-
{
|
|
3260
|
-
"code": "FILE_ASSETS_UNAVAILABLE",
|
|
3261
|
-
"message": f"record_get could not collect file assets: {exc}",
|
|
3262
|
-
}
|
|
3263
|
-
],
|
|
3569
|
+
"warnings": [warning],
|
|
3264
3570
|
}
|
|
3265
3571
|
|
|
3266
3572
|
def _record_get_refreshed_media_source_url(
|
|
@@ -3272,7 +3578,7 @@ class RecordTools(ToolBase):
|
|
|
3272
3578
|
resolved_view: AccessibleViewRoute,
|
|
3273
3579
|
audit_node_id: int | None,
|
|
3274
3580
|
candidate: JSONObject,
|
|
3275
|
-
) ->
|
|
3581
|
+
) -> JSONValue | None:
|
|
3276
3582
|
"""Refresh the detail payload once to recover an expired attachment storage signature."""
|
|
3277
3583
|
if candidate.get("source") not in {"attachment", "image_field", "subtable"}:
|
|
3278
3584
|
return None
|
|
@@ -3288,8 +3594,15 @@ class RecordTools(ToolBase):
|
|
|
3288
3594
|
resolved_view=resolved_view,
|
|
3289
3595
|
audit_node_id=audit_node_id,
|
|
3290
3596
|
)
|
|
3291
|
-
except QingflowApiError:
|
|
3292
|
-
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
|
+
}
|
|
3293
3606
|
for answer in _record_detail_answers(detail_result):
|
|
3294
3607
|
if not isinstance(answer, dict) or _coerce_count(answer.get("queId")) != field_id:
|
|
3295
3608
|
continue
|
|
@@ -3804,6 +4117,7 @@ class RecordTools(ToolBase):
|
|
|
3804
4117
|
record_id: Any | None,
|
|
3805
4118
|
fields: JSONObject | None = None,
|
|
3806
4119
|
items: list[JSONObject] | None = None,
|
|
4120
|
+
view_id: str | None = None,
|
|
3807
4121
|
dry_run: bool = False,
|
|
3808
4122
|
verify_write: bool = True,
|
|
3809
4123
|
output_profile: str = "normal",
|
|
@@ -3824,6 +4138,7 @@ class RecordTools(ToolBase):
|
|
|
3824
4138
|
profile=profile,
|
|
3825
4139
|
app_key=app_key,
|
|
3826
4140
|
items=normalized_items,
|
|
4141
|
+
view_id=view_id,
|
|
3827
4142
|
dry_run=dry_run,
|
|
3828
4143
|
verify_write=verify_write,
|
|
3829
4144
|
output_profile=normalized_output_profile,
|
|
@@ -3840,6 +4155,7 @@ class RecordTools(ToolBase):
|
|
|
3840
4155
|
app_key=app_key,
|
|
3841
4156
|
record_id=record_id_int,
|
|
3842
4157
|
fields=cast(JSONObject, fields or {}),
|
|
4158
|
+
view_id=view_id,
|
|
3843
4159
|
verify_write=verify_write,
|
|
3844
4160
|
output_profile=normalized_output_profile,
|
|
3845
4161
|
)
|
|
@@ -3851,17 +4167,61 @@ class RecordTools(ToolBase):
|
|
|
3851
4167
|
app_key: str,
|
|
3852
4168
|
record_id: int,
|
|
3853
4169
|
fields: JSONObject,
|
|
4170
|
+
view_id: str | None,
|
|
3854
4171
|
verify_write: bool,
|
|
3855
4172
|
output_profile: str,
|
|
4173
|
+
capture_exceptions: bool = False,
|
|
3856
4174
|
) -> JSONObject:
|
|
3857
4175
|
"""执行内部辅助逻辑。"""
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
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)
|
|
3865
4225
|
preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
|
|
3866
4226
|
preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
3867
4227
|
normalized_payload = self._record_write_normalized_payload(
|
|
@@ -3881,6 +4241,7 @@ class RecordTools(ToolBase):
|
|
|
3881
4241
|
human_review=True,
|
|
3882
4242
|
target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
|
|
3883
4243
|
)
|
|
4244
|
+
write_attempted_ref(True)
|
|
3884
4245
|
route_apply, tried_routes, route_blocker = self._record_update_apply_with_auto_route(
|
|
3885
4246
|
profile=profile,
|
|
3886
4247
|
app_key=app_key,
|
|
@@ -4161,7 +4522,9 @@ class RecordTools(ToolBase):
|
|
|
4161
4522
|
)
|
|
4162
4523
|
|
|
4163
4524
|
def _record_update_route_permission_denied(self, exc: QingflowApiError) -> bool:
|
|
4164
|
-
if exc
|
|
4525
|
+
if is_auth_like_error(exc):
|
|
4526
|
+
return False
|
|
4527
|
+
if backend_code_int(exc) in {40002, 40027, 40038, 404}:
|
|
4165
4528
|
return True
|
|
4166
4529
|
if exc.http_status == 404:
|
|
4167
4530
|
return True
|
|
@@ -4265,6 +4628,8 @@ class RecordTools(ToolBase):
|
|
|
4265
4628
|
},
|
|
4266
4629
|
)
|
|
4267
4630
|
except QingflowApiError as exc:
|
|
4631
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
4632
|
+
raise
|
|
4268
4633
|
return unavailable(
|
|
4269
4634
|
error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
|
|
4270
4635
|
reason="current-user todo task list is unavailable",
|
|
@@ -4312,6 +4677,8 @@ class RecordTools(ToolBase):
|
|
|
4312
4677
|
f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
|
|
4313
4678
|
)
|
|
4314
4679
|
except QingflowApiError as exc:
|
|
4680
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
4681
|
+
raise
|
|
4315
4682
|
return unavailable(
|
|
4316
4683
|
error_code="TASK_EDITABLE_FIELDS_UNAVAILABLE",
|
|
4317
4684
|
reason="workflow node editable field list is unavailable; record_update will not guess task editability",
|
|
@@ -4462,7 +4829,7 @@ class RecordTools(ToolBase):
|
|
|
4462
4829
|
raise_tool_error(QingflowApiError.config_error("view_key is required for custom view update"))
|
|
4463
4830
|
|
|
4464
4831
|
def runner(session_profile, context):
|
|
4465
|
-
index = self.
|
|
4832
|
+
index = self._get_view_field_index(profile, context, normalized_view_key, force_refresh=force_refresh_form) if verify_write else None
|
|
4466
4833
|
normalized_answers = [dict(item) for item in answers if isinstance(item, dict)]
|
|
4467
4834
|
self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
|
|
4468
4835
|
result = self.backend.request(
|
|
@@ -4565,6 +4932,7 @@ class RecordTools(ToolBase):
|
|
|
4565
4932
|
profile: str,
|
|
4566
4933
|
app_key: str,
|
|
4567
4934
|
items: list[JSONObject],
|
|
4935
|
+
view_id: str | None,
|
|
4568
4936
|
dry_run: bool,
|
|
4569
4937
|
verify_write: bool,
|
|
4570
4938
|
output_profile: str,
|
|
@@ -4576,6 +4944,7 @@ class RecordTools(ToolBase):
|
|
|
4576
4944
|
app_key=app_key,
|
|
4577
4945
|
record_id=cast(int, item["record_id"]),
|
|
4578
4946
|
fields=cast(JSONObject, item["fields"]),
|
|
4947
|
+
view_id=view_id,
|
|
4579
4948
|
output_profile=output_profile,
|
|
4580
4949
|
)
|
|
4581
4950
|
for item in items
|
|
@@ -4604,8 +4973,10 @@ class RecordTools(ToolBase):
|
|
|
4604
4973
|
app_key=app_key,
|
|
4605
4974
|
record_id=record_id,
|
|
4606
4975
|
fields=fields,
|
|
4976
|
+
view_id=view_id,
|
|
4607
4977
|
verify_write=verify_write,
|
|
4608
4978
|
output_profile=output_profile,
|
|
4979
|
+
capture_exceptions=True,
|
|
4609
4980
|
)
|
|
4610
4981
|
)
|
|
4611
4982
|
except (QingflowApiError, RuntimeError) as exc:
|
|
@@ -4636,16 +5007,20 @@ class RecordTools(ToolBase):
|
|
|
4636
5007
|
app_key: str,
|
|
4637
5008
|
record_id: int,
|
|
4638
5009
|
fields: JSONObject,
|
|
5010
|
+
view_id: str | None,
|
|
4639
5011
|
output_profile: str,
|
|
4640
5012
|
) -> JSONObject:
|
|
4641
5013
|
"""执行内部辅助逻辑。"""
|
|
4642
|
-
|
|
4643
|
-
profile
|
|
4644
|
-
app_key
|
|
4645
|
-
record_id
|
|
4646
|
-
fields
|
|
4647
|
-
force_refresh_form
|
|
4648
|
-
|
|
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)
|
|
4649
5024
|
preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
4650
5025
|
normalized_payload = self._record_write_normalized_payload(
|
|
4651
5026
|
operation="update",
|
|
@@ -4851,6 +5226,9 @@ class RecordTools(ToolBase):
|
|
|
4851
5226
|
item: JSONObject = {
|
|
4852
5227
|
"resource": data.get("resource"),
|
|
4853
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"),
|
|
4854
5232
|
"verification": data.get("verification"),
|
|
4855
5233
|
"field_errors": cast(list[JSONObject], data.get("field_errors", [])),
|
|
4856
5234
|
"confirmation_requests": cast(list[JSONObject], data.get("confirmation_requests", [])),
|
|
@@ -4860,7 +5238,7 @@ class RecordTools(ToolBase):
|
|
|
4860
5238
|
if isinstance(update_route, dict):
|
|
4861
5239
|
item["update_route"] = update_route
|
|
4862
5240
|
tried_routes = response.get("tried_routes")
|
|
4863
|
-
if isinstance(tried_routes, list):
|
|
5241
|
+
if isinstance(tried_routes, list) and (output_profile == "verbose" or response.get("status") != "success"):
|
|
4864
5242
|
item["tried_routes"] = tried_routes
|
|
4865
5243
|
blockers = data.get("blockers")
|
|
4866
5244
|
if isinstance(blockers, list) and blockers:
|
|
@@ -4882,6 +5260,7 @@ class RecordTools(ToolBase):
|
|
|
4882
5260
|
app_key: str,
|
|
4883
5261
|
record_id: int,
|
|
4884
5262
|
fields: JSONObject,
|
|
5263
|
+
preferred_view_id: str | None = None,
|
|
4885
5264
|
force_refresh_form: bool,
|
|
4886
5265
|
) -> JSONObject:
|
|
4887
5266
|
"""执行内部辅助逻辑。"""
|
|
@@ -4889,6 +5268,25 @@ class RecordTools(ToolBase):
|
|
|
4889
5268
|
request_route = self._request_route_payload(context)
|
|
4890
5269
|
def build_once(*, effective_force_refresh: bool) -> JSONObject:
|
|
4891
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)
|
|
4892
5290
|
probes = self._probe_candidate_record_contexts(
|
|
4893
5291
|
context,
|
|
4894
5292
|
app_key=app_key,
|
|
@@ -5102,41 +5500,24 @@ class RecordTools(ToolBase):
|
|
|
5102
5500
|
"data": first_confirmation_plan,
|
|
5103
5501
|
}
|
|
5104
5502
|
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
app_key=app_key,
|
|
5109
|
-
record_id=record_id,
|
|
5110
|
-
fields=fields,
|
|
5111
|
-
current_answers=matched_answers_for_union or [],
|
|
5112
|
-
matched_routes=matched_routes,
|
|
5113
|
-
force_refresh_form=effective_force_refresh,
|
|
5114
|
-
)
|
|
5115
|
-
if union_plan is not None:
|
|
5116
|
-
validation = union_plan.get("validation")
|
|
5117
|
-
if isinstance(validation, dict):
|
|
5118
|
-
warnings = validation.get("warnings")
|
|
5119
|
-
if not isinstance(warnings, list):
|
|
5120
|
-
warnings = []
|
|
5121
|
-
validation["warnings"] = warnings
|
|
5122
|
-
for message in fallback_warning_messages:
|
|
5123
|
-
if message not in warnings:
|
|
5124
|
-
warnings.append(message)
|
|
5125
|
-
union_plan["view_probe_summary"] = probe_summary
|
|
5126
|
-
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
|
|
5127
5506
|
return {
|
|
5128
5507
|
"profile": profile,
|
|
5129
5508
|
"ws_id": session_profile.selected_ws_id,
|
|
5130
5509
|
"ok": True,
|
|
5131
5510
|
"request_route": request_route,
|
|
5132
|
-
"data":
|
|
5511
|
+
"data": first_blocked_plan,
|
|
5133
5512
|
}
|
|
5134
5513
|
|
|
5135
5514
|
blocked_data = self._build_auto_view_blocked_preflight_data(
|
|
5136
5515
|
app_key=app_key,
|
|
5137
5516
|
record_id=record_id,
|
|
5138
5517
|
blockers=["NO_SINGLE_VIEW_CAN_UPDATE_ALL_FIELDS"],
|
|
5139
|
-
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
|
+
],
|
|
5140
5521
|
recommended_next_actions=[
|
|
5141
5522
|
"Call record_update_schema_get first to inspect the overall writable field set for this record.",
|
|
5142
5523
|
"Reduce the update payload until all requested fields fit inside one matched accessible view.",
|
|
@@ -5183,6 +5564,7 @@ class RecordTools(ToolBase):
|
|
|
5183
5564
|
union_writable_field_ids: set[int] = set()
|
|
5184
5565
|
union_visible_question_ids: set[int] = set()
|
|
5185
5566
|
matched_view_payloads: list[JSONObject] = []
|
|
5567
|
+
union_index: FieldIndex | None = None
|
|
5186
5568
|
|
|
5187
5569
|
for candidate in matched_routes:
|
|
5188
5570
|
browse_scope = self._build_browse_write_scope(
|
|
@@ -5192,11 +5574,13 @@ class RecordTools(ToolBase):
|
|
|
5192
5574
|
candidate,
|
|
5193
5575
|
force_refresh=force_refresh_form,
|
|
5194
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)
|
|
5195
5579
|
union_writable_field_ids.update(cast(set[int], browse_scope["writable_field_ids"]))
|
|
5196
5580
|
union_visible_question_ids.update(cast(set[int], browse_scope["visible_question_ids"]))
|
|
5197
5581
|
matched_view_payloads.append(_accessible_view_payload(candidate))
|
|
5198
5582
|
|
|
5199
|
-
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):
|
|
5200
5584
|
return None
|
|
5201
5585
|
|
|
5202
5586
|
plan_data = self._build_record_write_preflight(
|
|
@@ -5213,10 +5597,9 @@ class RecordTools(ToolBase):
|
|
|
5213
5597
|
view_key=None,
|
|
5214
5598
|
view_name=None,
|
|
5215
5599
|
existing_answers_override=current_answers,
|
|
5600
|
+
field_index_override=union_index,
|
|
5216
5601
|
)
|
|
5217
5602
|
|
|
5218
|
-
schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
|
|
5219
|
-
app_index = _build_applicant_top_level_field_index(schema)
|
|
5220
5603
|
validation = cast(JSONObject, plan_data.get("validation", {}))
|
|
5221
5604
|
invalid_fields = cast(list[JSONObject], validation.get("invalid_fields", []))
|
|
5222
5605
|
missing_required_fields = cast(list[JSONObject], validation.get("missing_required_fields", []))
|
|
@@ -5227,12 +5610,21 @@ class RecordTools(ToolBase):
|
|
|
5227
5610
|
invalid_fields.extend(
|
|
5228
5611
|
self._validate_view_scoped_subtable_answers(
|
|
5229
5612
|
normalized_answers=cast(list[JSONObject], plan_data.get("normalized_answers", [])),
|
|
5230
|
-
full_index=
|
|
5231
|
-
selector_index=
|
|
5613
|
+
full_index=union_index,
|
|
5614
|
+
selector_index=union_index,
|
|
5232
5615
|
visible_question_ids=union_visible_question_ids,
|
|
5233
5616
|
)
|
|
5234
5617
|
)
|
|
5235
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
|
+
]
|
|
5236
5628
|
existing_readonly_ids = {
|
|
5237
5629
|
str(_coerce_count(item.get("que_id")))
|
|
5238
5630
|
for item in readonly_or_system_fields
|
|
@@ -5396,7 +5788,13 @@ class RecordTools(ToolBase):
|
|
|
5396
5788
|
view_type=None,
|
|
5397
5789
|
)
|
|
5398
5790
|
)
|
|
5399
|
-
|
|
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:
|
|
5400
5798
|
if not isinstance(item, dict):
|
|
5401
5799
|
continue
|
|
5402
5800
|
view_key = _normalize_optional_text(item.get("viewKey"))
|
|
@@ -5453,7 +5851,9 @@ class RecordTools(ToolBase):
|
|
|
5453
5851
|
return payload
|
|
5454
5852
|
|
|
5455
5853
|
def _is_record_context_route_miss(self, error: QingflowApiError) -> bool:
|
|
5456
|
-
if error
|
|
5854
|
+
if is_auth_like_error(error):
|
|
5855
|
+
return False
|
|
5856
|
+
if backend_code_int(error) in {40002, 40023, 40027, 40038, 404}:
|
|
5457
5857
|
return True
|
|
5458
5858
|
if error.http_status == 404:
|
|
5459
5859
|
return True
|
|
@@ -5483,11 +5883,12 @@ class RecordTools(ToolBase):
|
|
|
5483
5883
|
used_list_type = None
|
|
5484
5884
|
else:
|
|
5485
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)
|
|
5486
5887
|
record = self.backend.request(
|
|
5487
5888
|
"GET",
|
|
5488
5889
|
context,
|
|
5489
5890
|
f"/app/{app_key}/apply/{apply_id}",
|
|
5490
|
-
params={"role":
|
|
5891
|
+
params={"role": role, "listType": used_list_type},
|
|
5491
5892
|
)
|
|
5492
5893
|
answers = record.get("answers") if isinstance(record, dict) else None
|
|
5493
5894
|
normalized_answers = [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []
|
|
@@ -5517,6 +5918,8 @@ class RecordTools(ToolBase):
|
|
|
5517
5918
|
error_payload=None,
|
|
5518
5919
|
)
|
|
5519
5920
|
except QingflowApiError as exc:
|
|
5921
|
+
if not self._is_record_context_route_miss(exc):
|
|
5922
|
+
raise
|
|
5520
5923
|
return RecordContextRouteProbe(
|
|
5521
5924
|
route=resolved_view,
|
|
5522
5925
|
answer_list=None,
|
|
@@ -5588,7 +5991,7 @@ class RecordTools(ToolBase):
|
|
|
5588
5991
|
]
|
|
5589
5992
|
|
|
5590
5993
|
def _looks_like_generic_record_update_backend_failure(self, exc: QingflowApiError) -> bool:
|
|
5591
|
-
if exc
|
|
5994
|
+
if backend_code_int(exc) == 500:
|
|
5592
5995
|
return True
|
|
5593
5996
|
if exc.http_status is not None and exc.http_status >= 500:
|
|
5594
5997
|
return True
|
|
@@ -5713,12 +6116,15 @@ class RecordTools(ToolBase):
|
|
|
5713
6116
|
app_key: str,
|
|
5714
6117
|
record_id: Any | None = None,
|
|
5715
6118
|
record_ids: list[Any] | None = None,
|
|
6119
|
+
view_id: str | None = None,
|
|
6120
|
+
list_type: int | None = None,
|
|
5716
6121
|
output_profile: str = "normal",
|
|
5717
6122
|
) -> JSONObject:
|
|
5718
6123
|
"""执行记录相关逻辑。"""
|
|
5719
6124
|
normalized_output_profile = self._normalize_public_output_profile(output_profile)
|
|
5720
6125
|
if not app_key:
|
|
5721
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)
|
|
5722
6128
|
normalized_record_ids: list[int] = []
|
|
5723
6129
|
for index, item in enumerate(record_ids or []):
|
|
5724
6130
|
normalized_record_ids.append(normalize_positive_id_int(item, field_name=f"record_ids[{index}]"))
|
|
@@ -5738,21 +6144,72 @@ class RecordTools(ToolBase):
|
|
|
5738
6144
|
"record_ids": [stringify_backend_id(item) for item in delete_ids],
|
|
5739
6145
|
"answers": [],
|
|
5740
6146
|
"submit_type": 1,
|
|
6147
|
+
"selection": {"view_id": view_id, "list_type": delete_list_type},
|
|
5741
6148
|
}
|
|
5742
6149
|
return self._record_delete_public_batch(
|
|
5743
6150
|
profile=profile,
|
|
5744
6151
|
app_key=app_key,
|
|
5745
6152
|
delete_ids=delete_ids,
|
|
6153
|
+
list_type=delete_list_type,
|
|
5746
6154
|
normalized_payload=normalized_payload,
|
|
5747
6155
|
output_profile=normalized_output_profile,
|
|
5748
6156
|
)
|
|
5749
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
|
+
|
|
5750
6206
|
def _record_delete_public_batch(
|
|
5751
6207
|
self,
|
|
5752
6208
|
*,
|
|
5753
6209
|
profile: str,
|
|
5754
6210
|
app_key: str,
|
|
5755
6211
|
delete_ids: list[int],
|
|
6212
|
+
list_type: int,
|
|
5756
6213
|
normalized_payload: JSONObject,
|
|
5757
6214
|
output_profile: str,
|
|
5758
6215
|
) -> JSONObject:
|
|
@@ -5762,7 +6219,7 @@ class RecordTools(ToolBase):
|
|
|
5762
6219
|
for index, delete_id in enumerate(delete_ids):
|
|
5763
6220
|
record_id_text = stringify_backend_id(delete_id)
|
|
5764
6221
|
try:
|
|
5765
|
-
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)
|
|
5766
6223
|
request_route = cast(JSONObject, raw_apply.get("request_route")) if isinstance(raw_apply.get("request_route"), dict) else request_route
|
|
5767
6224
|
ws_id = raw_apply.get("ws_id", ws_id)
|
|
5768
6225
|
single_payload = {
|
|
@@ -5771,6 +6228,7 @@ class RecordTools(ToolBase):
|
|
|
5771
6228
|
"record_ids": [record_id_text],
|
|
5772
6229
|
"answers": [],
|
|
5773
6230
|
"submit_type": 1,
|
|
6231
|
+
"selection": normalized_payload.get("selection"),
|
|
5774
6232
|
}
|
|
5775
6233
|
single_response = self._record_write_apply_response(
|
|
5776
6234
|
raw_apply,
|
|
@@ -6053,12 +6511,13 @@ class RecordTools(ToolBase):
|
|
|
6053
6511
|
preflight=raw_preflight,
|
|
6054
6512
|
)
|
|
6055
6513
|
|
|
6056
|
-
if
|
|
6514
|
+
if view_key is not None or view_name is not None:
|
|
6057
6515
|
raise_tool_error(
|
|
6058
6516
|
QingflowApiError.config_error(
|
|
6059
|
-
"delete does not
|
|
6517
|
+
"delete does not support custom view selectors; use a system view_id/list_type or resolve target record_ids first"
|
|
6060
6518
|
)
|
|
6061
6519
|
)
|
|
6520
|
+
delete_list_type = self._resolve_record_delete_list_type(view_id=view_id, list_type=list_type)
|
|
6062
6521
|
if normalized_values or normalized_set:
|
|
6063
6522
|
raise_tool_error(QingflowApiError.config_error("delete must not include values or set"))
|
|
6064
6523
|
delete_ids = normalized_record_ids or ([record_id] if record_id is not None and record_id > 0 else [])
|
|
@@ -6070,8 +6529,9 @@ class RecordTools(ToolBase):
|
|
|
6070
6529
|
"record_ids": delete_ids,
|
|
6071
6530
|
"answers": [],
|
|
6072
6531
|
"submit_type": submit_type_value,
|
|
6532
|
+
"selection": {"view_id": view_id, "list_type": delete_list_type},
|
|
6073
6533
|
}
|
|
6074
|
-
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)
|
|
6075
6535
|
return self._record_write_apply_response(
|
|
6076
6536
|
raw_apply,
|
|
6077
6537
|
operation="delete",
|
|
@@ -6221,7 +6681,9 @@ class RecordTools(ToolBase):
|
|
|
6221
6681
|
or _normalize_optional_text(payload.get("appName"))
|
|
6222
6682
|
or _normalize_optional_text(payload.get("appTitle"))
|
|
6223
6683
|
)
|
|
6224
|
-
except QingflowApiError:
|
|
6684
|
+
except QingflowApiError as exc:
|
|
6685
|
+
if is_auth_like_error(exc):
|
|
6686
|
+
raise
|
|
6225
6687
|
name = None
|
|
6226
6688
|
self._app_name_cache[cache_key] = name
|
|
6227
6689
|
return name
|
|
@@ -6375,7 +6837,9 @@ class RecordTools(ToolBase):
|
|
|
6375
6837
|
try:
|
|
6376
6838
|
result = self.backend.request("GET", context, "/data/baseInfo", params={"queId": field.que_id})
|
|
6377
6839
|
payload = result if isinstance(result, dict) else None
|
|
6378
|
-
except QingflowApiError:
|
|
6840
|
+
except QingflowApiError as exc:
|
|
6841
|
+
if is_auth_like_error(exc):
|
|
6842
|
+
raise
|
|
6379
6843
|
payload = None
|
|
6380
6844
|
self._relation_base_info_cache[cache_key] = payload or {}
|
|
6381
6845
|
return payload
|
|
@@ -6648,6 +7112,26 @@ class RecordTools(ToolBase):
|
|
|
6648
7112
|
or bool(fields)
|
|
6649
7113
|
)
|
|
6650
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
|
+
|
|
6651
7135
|
def _build_candidate_lookup_state(
|
|
6652
7136
|
self,
|
|
6653
7137
|
profile: str,
|
|
@@ -6666,7 +7150,9 @@ class RecordTools(ToolBase):
|
|
|
6666
7150
|
if apply_id is not None:
|
|
6667
7151
|
try:
|
|
6668
7152
|
base_answers = self._load_record_answers_for_preflight(context, app_key=app_key, apply_id=apply_id)
|
|
6669
|
-
except QingflowApiError:
|
|
7153
|
+
except QingflowApiError as exc:
|
|
7154
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
7155
|
+
raise
|
|
6670
7156
|
context_complete = False
|
|
6671
7157
|
state = LookupResolutionState(
|
|
6672
7158
|
operation="update" if apply_id is not None else "insert",
|
|
@@ -7156,15 +7642,16 @@ class RecordTools(ToolBase):
|
|
|
7156
7642
|
)
|
|
7157
7643
|
if configured_candidate is not None:
|
|
7158
7644
|
self._merge_department_candidate(merged, configured_candidate)
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
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)
|
|
7168
7655
|
filtered = _filter_department_candidates(list(merged.values()), keyword)
|
|
7169
7656
|
filtered.sort(key=lambda item: (_normalize_optional_text(item.get("value")) or "", _coerce_count(item.get("id")) or 0))
|
|
7170
7657
|
return filtered
|
|
@@ -8305,22 +8792,10 @@ class RecordTools(ToolBase):
|
|
|
8305
8792
|
field_index_override: FieldIndex | None = None,
|
|
8306
8793
|
) -> JSONObject:
|
|
8307
8794
|
"""执行内部辅助逻辑。"""
|
|
8308
|
-
schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
|
|
8309
|
-
base_index = field_index_override or _build_applicant_top_level_field_index(schema)
|
|
8310
|
-
question_relations = _collect_question_relations(schema)
|
|
8311
|
-
runtime_linked_field_ids = _collect_linked_required_field_ids(question_relations)
|
|
8312
|
-
runtime_linked_field_ids.update(_collect_option_linked_field_ids(base_index))
|
|
8313
|
-
index = base_index
|
|
8314
|
-
if operation == "create" and field_index_override is None:
|
|
8315
|
-
linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
|
|
8316
|
-
schema,
|
|
8317
|
-
linked_field_ids=runtime_linked_field_ids,
|
|
8318
|
-
)
|
|
8319
|
-
index = _merge_field_indexes(base_index, linked_hidden_index)
|
|
8320
8795
|
normalized_fields = fields or {}
|
|
8321
8796
|
normalized_answers_input = answers or []
|
|
8322
8797
|
resolved_view: AccessibleViewRoute | None = None
|
|
8323
|
-
selector_index =
|
|
8798
|
+
selector_index: FieldIndex | None = field_index_override
|
|
8324
8799
|
browse_writable_field_ids: set[int] = set()
|
|
8325
8800
|
visible_question_ids: set[int] = set()
|
|
8326
8801
|
if any(item is not None for item in (view_id, list_type, view_key, view_name)):
|
|
@@ -8346,6 +8821,31 @@ class RecordTools(ToolBase):
|
|
|
8346
8821
|
visible_question_ids = cast(set[int], browse_scope["visible_question_ids"])
|
|
8347
8822
|
else:
|
|
8348
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
|
|
8349
8849
|
resolved_fields = self._collect_write_plan_field_refs(fields=normalized_fields, answers=normalized_answers_input, index=selector_index)
|
|
8350
8850
|
support_matrix = _summarize_write_support(resolved_fields)
|
|
8351
8851
|
invalid_fields: list[JSONObject] = []
|
|
@@ -8389,7 +8889,9 @@ class RecordTools(ToolBase):
|
|
|
8389
8889
|
apply_id=apply_id,
|
|
8390
8890
|
)
|
|
8391
8891
|
existing_answers_loaded = True
|
|
8392
|
-
except QingflowApiError:
|
|
8892
|
+
except QingflowApiError as exc:
|
|
8893
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
8894
|
+
raise
|
|
8393
8895
|
validation_warnings.append(
|
|
8394
8896
|
"update preflight could not load the current record; required-field completeness and dynamic lookup context were not fully revalidated."
|
|
8395
8897
|
)
|
|
@@ -8978,7 +9480,7 @@ class RecordTools(ToolBase):
|
|
|
8978
9480
|
break
|
|
8979
9481
|
except QingflowApiError as exc:
|
|
8980
9482
|
last_error = exc
|
|
8981
|
-
if exc
|
|
9483
|
+
if _is_record_permission_denied_error(exc):
|
|
8982
9484
|
continue
|
|
8983
9485
|
raise
|
|
8984
9486
|
if result is None:
|
|
@@ -9081,7 +9583,21 @@ class RecordTools(ToolBase):
|
|
|
9081
9583
|
normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
|
|
9082
9584
|
|
|
9083
9585
|
def runner(session_profile, context):
|
|
9084
|
-
|
|
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
|
|
9085
9601
|
normalized_answers = self._resolve_answers(
|
|
9086
9602
|
profile,
|
|
9087
9603
|
context,
|
|
@@ -9089,6 +9605,7 @@ class RecordTools(ToolBase):
|
|
|
9089
9605
|
answers=answers or [],
|
|
9090
9606
|
fields=fields or {},
|
|
9091
9607
|
force_refresh_form=force_refresh_form,
|
|
9608
|
+
field_index_override=update_index,
|
|
9092
9609
|
)
|
|
9093
9610
|
self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
|
|
9094
9611
|
try:
|
|
@@ -9142,13 +9659,14 @@ class RecordTools(ToolBase):
|
|
|
9142
9659
|
def record_delete(self, *, profile: str, app_key: str, apply_id: int, list_type: int) -> JSONObject:
|
|
9143
9660
|
"""执行记录相关逻辑。"""
|
|
9144
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)
|
|
9145
9663
|
|
|
9146
9664
|
def runner(session_profile, context):
|
|
9147
9665
|
result = self.backend.request(
|
|
9148
9666
|
"DELETE",
|
|
9149
9667
|
context,
|
|
9150
9668
|
f"/app/{app_key}/apply",
|
|
9151
|
-
json_body={"type":
|
|
9669
|
+
json_body={"type": delete_list_type, "applyIds": [normalized_apply_id]},
|
|
9152
9670
|
)
|
|
9153
9671
|
return self._attach_human_review_notice(
|
|
9154
9672
|
{
|
|
@@ -9157,6 +9675,7 @@ class RecordTools(ToolBase):
|
|
|
9157
9675
|
"request_route": self._request_route_payload(context),
|
|
9158
9676
|
"app_key": app_key,
|
|
9159
9677
|
"apply_id": normalized_apply_id,
|
|
9678
|
+
"list_type": delete_list_type,
|
|
9160
9679
|
"result": result,
|
|
9161
9680
|
},
|
|
9162
9681
|
operation="delete",
|
|
@@ -9201,7 +9720,7 @@ class RecordTools(ToolBase):
|
|
|
9201
9720
|
"GET",
|
|
9202
9721
|
context,
|
|
9203
9722
|
f"/app/{app_key}/apply/{apply_id}",
|
|
9204
|
-
params={"role":
|
|
9723
|
+
params={"role": _record_detail_role_for_list_type(list_type), "listType": list_type},
|
|
9205
9724
|
)
|
|
9206
9725
|
answers = result.get("answers") if isinstance(result, dict) else None
|
|
9207
9726
|
answer_list = answers if isinstance(answers, list) else []
|
|
@@ -9560,7 +10079,7 @@ class RecordTools(ToolBase):
|
|
|
9560
10079
|
used_list_type: int | None = None
|
|
9561
10080
|
if view_selection is not None:
|
|
9562
10081
|
fallback_list_types = [view_route.list_type if view_route.list_type is not None else DEFAULT_RECORD_LIST_TYPE]
|
|
9563
|
-
elif view_route.list_type is not None
|
|
10082
|
+
elif view_route.list_type is not None:
|
|
9564
10083
|
fallback_list_types = [view_route.list_type]
|
|
9565
10084
|
else:
|
|
9566
10085
|
fallback_list_types = [DEFAULT_RECORD_LIST_TYPE, 14, 1, 2, 12]
|
|
@@ -9791,7 +10310,7 @@ class RecordTools(ToolBase):
|
|
|
9791
10310
|
try:
|
|
9792
10311
|
payload = self.backend.request("GET", context, f"/view/{view_key}/viewConfig")
|
|
9793
10312
|
except QingflowApiError as exc:
|
|
9794
|
-
if exc
|
|
10313
|
+
if _is_optional_schema_permission_error(exc):
|
|
9795
10314
|
self._view_config_cache[cache_key] = None
|
|
9796
10315
|
return None
|
|
9797
10316
|
raise
|
|
@@ -9912,7 +10431,12 @@ class RecordTools(ToolBase):
|
|
|
9912
10431
|
)
|
|
9913
10432
|
normalized = _normalize_data_list_base_info_schema(payload)
|
|
9914
10433
|
if not isinstance(normalized.get("formQues"), list) or not normalized.get("formQues"):
|
|
9915
|
-
|
|
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
|
|
9916
10440
|
self._form_cache[cache_key] = normalized
|
|
9917
10441
|
return normalized
|
|
9918
10442
|
|
|
@@ -9944,8 +10468,16 @@ class RecordTools(ToolBase):
|
|
|
9944
10468
|
cache_key = (profile, f"view:{view_key}", "browse_view_base_info", None)
|
|
9945
10469
|
if not force_refresh and cache_key in self._form_cache:
|
|
9946
10470
|
return self._form_cache[cache_key]
|
|
9947
|
-
|
|
9948
|
-
|
|
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)
|
|
9949
10481
|
self._form_cache[cache_key] = normalized
|
|
9950
10482
|
return normalized
|
|
9951
10483
|
|
|
@@ -10001,22 +10533,6 @@ class RecordTools(ToolBase):
|
|
|
10001
10533
|
force_refresh: bool,
|
|
10002
10534
|
) -> JSONObject:
|
|
10003
10535
|
"""Build the UI/table-view readable field scope from apply/baseInfo."""
|
|
10004
|
-
applicant_index: FieldIndex | None
|
|
10005
|
-
applicant_writable_field_ids: set[int]
|
|
10006
|
-
try:
|
|
10007
|
-
applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
|
|
10008
|
-
except QingflowApiError as exc:
|
|
10009
|
-
if exc.backend_code != 40002:
|
|
10010
|
-
raise
|
|
10011
|
-
applicant_index = None
|
|
10012
|
-
applicant_writable_field_ids = set()
|
|
10013
|
-
else:
|
|
10014
|
-
applicant_writable_field_ids = {
|
|
10015
|
-
field.que_id
|
|
10016
|
-
for field in applicant_index.by_id.values()
|
|
10017
|
-
if bool(self._schema_write_hints(field)["writable"])
|
|
10018
|
-
}
|
|
10019
|
-
|
|
10020
10536
|
if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
10021
10537
|
schema = self._get_custom_view_browse_schema(
|
|
10022
10538
|
profile,
|
|
@@ -10025,6 +10541,16 @@ class RecordTools(ToolBase):
|
|
|
10025
10541
|
force_refresh=force_refresh,
|
|
10026
10542
|
)
|
|
10027
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
|
+
}
|
|
10028
10554
|
elif resolved_view is not None and resolved_view.kind == "system" and resolved_view.list_type is not None:
|
|
10029
10555
|
schema = self._get_system_browse_base_info_schema(
|
|
10030
10556
|
profile,
|
|
@@ -10034,34 +10560,26 @@ class RecordTools(ToolBase):
|
|
|
10034
10560
|
force_refresh=force_refresh,
|
|
10035
10561
|
)
|
|
10036
10562
|
index = _build_top_level_field_index(schema)
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
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
|
+
}
|
|
10048
10573
|
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
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": {
|
|
10052
10579
|
field.que_id
|
|
10053
|
-
for field in
|
|
10580
|
+
for field in applicant_index.by_id.values()
|
|
10054
10581
|
if bool(self._schema_write_hints(field)["writable"])
|
|
10055
|
-
}
|
|
10056
|
-
else:
|
|
10057
|
-
writable_field_ids = {
|
|
10058
|
-
field_id
|
|
10059
|
-
for field_id in visible_question_ids
|
|
10060
|
-
if field_id in applicant_writable_field_ids
|
|
10061
|
-
}
|
|
10062
|
-
return {
|
|
10063
|
-
"index": index,
|
|
10064
|
-
"writable_field_ids": writable_field_ids,
|
|
10582
|
+
},
|
|
10065
10583
|
"visible_question_ids": visible_question_ids,
|
|
10066
10584
|
}
|
|
10067
10585
|
|
|
@@ -10075,23 +10593,13 @@ class RecordTools(ToolBase):
|
|
|
10075
10593
|
force_refresh: bool,
|
|
10076
10594
|
) -> JSONObject:
|
|
10077
10595
|
"""执行内部辅助逻辑。"""
|
|
10078
|
-
applicant_index: FieldIndex | None
|
|
10079
|
-
applicant_writable_field_ids: set[int]
|
|
10080
|
-
try:
|
|
10081
|
-
applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
|
|
10082
|
-
except QingflowApiError as exc:
|
|
10083
|
-
if exc.backend_code != 40002:
|
|
10084
|
-
raise
|
|
10085
|
-
applicant_index = None
|
|
10086
|
-
applicant_writable_field_ids = set()
|
|
10087
|
-
else:
|
|
10088
|
-
applicant_writable_field_ids = {
|
|
10089
|
-
field.que_id
|
|
10090
|
-
for field in applicant_index.by_id.values()
|
|
10091
|
-
if bool(self._schema_write_hints(field)["writable"])
|
|
10092
|
-
}
|
|
10093
10596
|
if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
10094
|
-
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
|
+
)
|
|
10095
10603
|
index = _build_top_level_field_index(schema)
|
|
10096
10604
|
visible_question_ids = self._get_view_question_ids(profile, context, resolved_view.view_selection.view_key)
|
|
10097
10605
|
if not visible_question_ids:
|
|
@@ -10107,6 +10615,12 @@ class RecordTools(ToolBase):
|
|
|
10107
10615
|
index = _build_top_level_field_index(schema)
|
|
10108
10616
|
visible_question_ids = _question_ids_from_schema(schema)
|
|
10109
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
|
+
}
|
|
10110
10624
|
index = applicant_index or _build_top_level_field_index(
|
|
10111
10625
|
self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
|
|
10112
10626
|
)
|
|
@@ -10125,43 +10639,13 @@ class RecordTools(ToolBase):
|
|
|
10125
10639
|
"visible_question_ids": set(visible_question_ids),
|
|
10126
10640
|
}
|
|
10127
10641
|
|
|
10128
|
-
if applicant_index is None:
|
|
10129
|
-
return {
|
|
10130
|
-
"index": index,
|
|
10131
|
-
"writable_field_ids": {
|
|
10132
|
-
field.que_id
|
|
10133
|
-
for field in index.by_id.values()
|
|
10134
|
-
if bool(self._schema_write_hints(field)["writable"])
|
|
10135
|
-
},
|
|
10136
|
-
"visible_question_ids": visible_question_ids,
|
|
10137
|
-
}
|
|
10138
|
-
|
|
10139
|
-
augmented_fields = [
|
|
10140
|
-
_clone_form_field(applicant_index.by_id.get(str(field.que_id)) or field)
|
|
10141
|
-
for field in index.by_id.values()
|
|
10142
|
-
]
|
|
10143
|
-
augmented_field_ids = {field.que_id for field in augmented_fields}
|
|
10144
|
-
writable_field_ids = {
|
|
10145
|
-
field_id
|
|
10146
|
-
for field_id in visible_question_ids
|
|
10147
|
-
if field_id in applicant_writable_field_ids
|
|
10148
|
-
}
|
|
10149
|
-
for field in applicant_index.by_id.values():
|
|
10150
|
-
descendant_ids = _subtable_descendant_ids(field)
|
|
10151
|
-
field_visible = field.que_id in visible_question_ids
|
|
10152
|
-
descendant_visible = bool(descendant_ids and (descendant_ids & visible_question_ids))
|
|
10153
|
-
if not field_visible and not descendant_visible:
|
|
10154
|
-
continue
|
|
10155
|
-
if field.que_id not in augmented_field_ids:
|
|
10156
|
-
augmented_fields.append(_clone_form_field(field))
|
|
10157
|
-
augmented_field_ids.add(field.que_id)
|
|
10158
|
-
if descendant_visible:
|
|
10159
|
-
visible_question_ids.add(field.que_id)
|
|
10160
|
-
if field.que_id in applicant_writable_field_ids and (field_visible or descendant_visible):
|
|
10161
|
-
writable_field_ids.add(field.que_id)
|
|
10162
10642
|
return {
|
|
10163
|
-
"index":
|
|
10164
|
-
"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
|
+
},
|
|
10165
10649
|
"visible_question_ids": visible_question_ids,
|
|
10166
10650
|
}
|
|
10167
10651
|
|
|
@@ -10226,7 +10710,7 @@ class RecordTools(ToolBase):
|
|
|
10226
10710
|
try:
|
|
10227
10711
|
payload = self.backend.request("GET", context, f"/view/{view_key}/question")
|
|
10228
10712
|
except QingflowApiError as exc:
|
|
10229
|
-
if exc
|
|
10713
|
+
if _is_record_permission_denied_error(exc):
|
|
10230
10714
|
return set()
|
|
10231
10715
|
raise
|
|
10232
10716
|
if not isinstance(payload, list):
|
|
@@ -10270,7 +10754,7 @@ class RecordTools(ToolBase):
|
|
|
10270
10754
|
)
|
|
10271
10755
|
return True
|
|
10272
10756
|
except QingflowApiError as exc:
|
|
10273
|
-
if exc
|
|
10757
|
+
if _is_record_permission_denied_error(exc):
|
|
10274
10758
|
return False
|
|
10275
10759
|
raise
|
|
10276
10760
|
|
|
@@ -10423,7 +10907,12 @@ class RecordTools(ToolBase):
|
|
|
10423
10907
|
requested_name = _normalize_optional_text(view_name)
|
|
10424
10908
|
if requested_key is None and requested_name is None:
|
|
10425
10909
|
return None
|
|
10426
|
-
|
|
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 = []
|
|
10427
10916
|
selected: JSONObject | None = None
|
|
10428
10917
|
if requested_key is not None:
|
|
10429
10918
|
selected = next((item for item in views if _normalize_optional_text(item.get("viewKey")) == requested_key), None)
|
|
@@ -10621,9 +11110,11 @@ class RecordTools(ToolBase):
|
|
|
10621
11110
|
|
|
10622
11111
|
def _should_retry_list_type_fallback(self, error: QingflowApiError) -> bool:
|
|
10623
11112
|
"""执行内部辅助逻辑。"""
|
|
10624
|
-
if error
|
|
11113
|
+
if is_auth_like_error(error):
|
|
11114
|
+
return False
|
|
11115
|
+
if backend_code_int(error) in {40002, 40027, 404}:
|
|
10625
11116
|
return True
|
|
10626
|
-
if error.http_status
|
|
11117
|
+
if error.http_status == 404:
|
|
10627
11118
|
return True
|
|
10628
11119
|
return False
|
|
10629
11120
|
|
|
@@ -11444,6 +11935,70 @@ class RecordTools(ToolBase):
|
|
|
11444
11935
|
)
|
|
11445
11936
|
)
|
|
11446
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
|
+
|
|
11447
12002
|
def _request_route_payload(self, context) -> JSONObject: # type: ignore[no-untyped-def]
|
|
11448
12003
|
"""执行内部辅助逻辑。"""
|
|
11449
12004
|
describe_route = getattr(self.backend, "describe_route", None)
|
|
@@ -11617,7 +12172,7 @@ class RecordTools(ToolBase):
|
|
|
11617
12172
|
selection: JSONObject | None,
|
|
11618
12173
|
) -> None:
|
|
11619
12174
|
"""执行内部辅助逻辑。"""
|
|
11620
|
-
if exc
|
|
12175
|
+
if not _is_record_permission_denied_error(exc):
|
|
11621
12176
|
raise exc
|
|
11622
12177
|
raise_tool_error(
|
|
11623
12178
|
QingflowApiError(
|
|
@@ -11783,6 +12338,7 @@ class RecordTools(ToolBase):
|
|
|
11783
12338
|
response_status = raw_status or "failed"
|
|
11784
12339
|
update_route = raw_apply.get("update_route") if isinstance(raw_apply.get("update_route"), dict) else None
|
|
11785
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"
|
|
11786
12342
|
response: JSONObject = {
|
|
11787
12343
|
"profile": raw_apply.get("profile"),
|
|
11788
12344
|
"ws_id": raw_apply.get("ws_id"),
|
|
@@ -11795,7 +12351,6 @@ class RecordTools(ToolBase):
|
|
|
11795
12351
|
"warnings": warnings,
|
|
11796
12352
|
"output_profile": output_profile,
|
|
11797
12353
|
"update_route": update_route,
|
|
11798
|
-
"tried_routes": tried_routes,
|
|
11799
12354
|
"data": {
|
|
11800
12355
|
"action": {"operation": operation, "executed": True},
|
|
11801
12356
|
"resource": resource,
|
|
@@ -11807,9 +12362,11 @@ class RecordTools(ToolBase):
|
|
|
11807
12362
|
"resolved_fields": resolved_fields,
|
|
11808
12363
|
"human_review": self._record_write_human_review_payload(operation, enabled=human_review),
|
|
11809
12364
|
"update_route": update_route,
|
|
11810
|
-
"tried_routes": tried_routes,
|
|
11811
12365
|
},
|
|
11812
12366
|
}
|
|
12367
|
+
if expose_tried_routes:
|
|
12368
|
+
response["tried_routes"] = tried_routes
|
|
12369
|
+
response["data"]["tried_routes"] = tried_routes
|
|
11813
12370
|
if record_id is not None:
|
|
11814
12371
|
response["record_id"] = record_id
|
|
11815
12372
|
if apply_id is not None:
|
|
@@ -11989,7 +12546,7 @@ class RecordTools(ToolBase):
|
|
|
11989
12546
|
)
|
|
11990
12547
|
return errors
|
|
11991
12548
|
|
|
11992
|
-
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:
|
|
11993
12550
|
"""执行内部辅助逻辑。"""
|
|
11994
12551
|
if not app_key:
|
|
11995
12552
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
@@ -12002,14 +12559,14 @@ class RecordTools(ToolBase):
|
|
|
12002
12559
|
"DELETE",
|
|
12003
12560
|
context,
|
|
12004
12561
|
f"/app/{app_key}/apply",
|
|
12005
|
-
json_body={"type":
|
|
12562
|
+
json_body={"type": list_type, "applyIds": normalized_ids},
|
|
12006
12563
|
)
|
|
12007
12564
|
return {
|
|
12008
12565
|
"profile": profile,
|
|
12009
12566
|
"ws_id": session_profile.selected_ws_id,
|
|
12010
12567
|
"request_route": self._request_route_payload(context),
|
|
12011
12568
|
"result": result,
|
|
12012
|
-
"resource": {"type": "record", "apply_ids": normalized_ids},
|
|
12569
|
+
"resource": {"type": "record", "apply_ids": normalized_ids, "list_type": list_type},
|
|
12013
12570
|
"ok": True,
|
|
12014
12571
|
}
|
|
12015
12572
|
|
|
@@ -12583,6 +13140,30 @@ class RecordTools(ToolBase):
|
|
|
12583
13140
|
},
|
|
12584
13141
|
)
|
|
12585
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
|
+
|
|
12586
13167
|
def _candidate_keyword_from_value(
|
|
12587
13168
|
self,
|
|
12588
13169
|
value: JSONValue,
|
|
@@ -12847,14 +13428,7 @@ class RecordTools(ToolBase):
|
|
|
12847
13428
|
|
|
12848
13429
|
def _lookup_department_detail(self, context, keyword: str, *, purpose: str) -> JSONObject: # type: ignore[no-untyped-def]
|
|
12849
13430
|
"""执行内部辅助逻辑。"""
|
|
12850
|
-
|
|
12851
|
-
"GET",
|
|
12852
|
-
context,
|
|
12853
|
-
"/contact/deptByPage",
|
|
12854
|
-
params={"keyword": keyword, "pageNum": 1, "pageSize": 20},
|
|
12855
|
-
)
|
|
12856
|
-
rows = payload.get("list") if isinstance(payload, dict) else None
|
|
12857
|
-
items = [item for item in rows if isinstance(item, dict)] if isinstance(rows, list) else []
|
|
13431
|
+
items = self._search_workspace_departments(context, keyword=keyword)
|
|
12858
13432
|
normalized_keyword = keyword.strip()
|
|
12859
13433
|
exact = [
|
|
12860
13434
|
item for item in items
|
|
@@ -13379,6 +13953,7 @@ class RecordTools(ToolBase):
|
|
|
13379
13953
|
normalized_answers: list[JSONObject],
|
|
13380
13954
|
index: FieldIndex,
|
|
13381
13955
|
verify_list_type: int = DEFAULT_RECORD_LIST_TYPE,
|
|
13956
|
+
verify_role: int | None = None,
|
|
13382
13957
|
verify_view_key: str | None = None,
|
|
13383
13958
|
) -> JSONObject:
|
|
13384
13959
|
"""执行内部辅助逻辑。"""
|
|
@@ -13398,14 +13973,20 @@ class RecordTools(ToolBase):
|
|
|
13398
13973
|
f"/view/{verify_view_key}/apply/{apply_id}",
|
|
13399
13974
|
)
|
|
13400
13975
|
else:
|
|
13976
|
+
role = verify_role if verify_role is not None else 1
|
|
13401
13977
|
record = self.backend.request(
|
|
13402
13978
|
"GET",
|
|
13403
13979
|
context,
|
|
13404
13980
|
f"/app/{app_key}/apply/{apply_id}",
|
|
13405
|
-
params={"role":
|
|
13981
|
+
params={"role": role, "listType": verify_list_type},
|
|
13406
13982
|
)
|
|
13407
13983
|
except QingflowApiError as exc:
|
|
13408
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))
|
|
13409
13990
|
return {
|
|
13410
13991
|
"verified": False,
|
|
13411
13992
|
"verification_mode": "custom_view_record_detail",
|
|
@@ -13414,14 +13995,9 @@ class RecordTools(ToolBase):
|
|
|
13414
13995
|
"missing_fields": [],
|
|
13415
13996
|
"empty_fields": [],
|
|
13416
13997
|
"count_mismatches": [],
|
|
13417
|
-
"warnings": [
|
|
13418
|
-
"code": "WRITE_VERIFY_CUSTOM_VIEW_READBACK_FAILED",
|
|
13419
|
-
"message": "Write was sent through a custom view route, but the same view could not be re-read for field-level verification.",
|
|
13420
|
-
"backend_code": exc.backend_code,
|
|
13421
|
-
"http_status": exc.http_status,
|
|
13422
|
-
}],
|
|
13998
|
+
"warnings": [warning],
|
|
13423
13999
|
}
|
|
13424
|
-
if exc
|
|
14000
|
+
if not _is_record_permission_denied_error(exc):
|
|
13425
14001
|
raise
|
|
13426
14002
|
return self._verify_record_write_result_via_initiated_tasks(
|
|
13427
14003
|
context,
|
|
@@ -13715,6 +14291,8 @@ def _normalize_data_list_base_info_schema(payload: JSONValue) -> JSONObject:
|
|
|
13715
14291
|
if not isinstance(payload, dict):
|
|
13716
14292
|
return {}
|
|
13717
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")
|
|
13718
14296
|
if not isinstance(que_base_infos, list):
|
|
13719
14297
|
return {}
|
|
13720
14298
|
return {
|
|
@@ -14101,6 +14679,44 @@ def _build_answer_backed_field_index(
|
|
|
14101
14679
|
)
|
|
14102
14680
|
|
|
14103
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
|
+
|
|
14104
14720
|
def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
|
|
14105
14721
|
by_id = dict(primary.by_id)
|
|
14106
14722
|
by_title = {key: list(value) for key, value in primary.by_title.items()}
|
|
@@ -14111,12 +14727,42 @@ def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
|
|
|
14111
14727
|
|
|
14112
14728
|
for field_id, field in extra.by_id.items():
|
|
14113
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)
|
|
14114
14735
|
continue
|
|
14115
14736
|
by_id[field_id] = field
|
|
14116
14737
|
by_title.setdefault(_normalize_field_lookup_key(field.que_title), []).append(field)
|
|
14117
14738
|
for alias in field.aliases:
|
|
14118
14739
|
by_alias.setdefault(_normalize_field_lookup_key(alias), []).append(field)
|
|
14119
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
|
+
|
|
14120
14766
|
return FieldIndex(
|
|
14121
14767
|
by_id=by_id,
|
|
14122
14768
|
by_title=by_title,
|
|
@@ -15507,6 +16153,28 @@ def _record_detail_unavailable_context(section: str, message: str, exc: Qingflow
|
|
|
15507
16153
|
"message": message,
|
|
15508
16154
|
"category": exc.category,
|
|
15509
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"
|
|
15510
16178
|
if exc.backend_code is not None:
|
|
15511
16179
|
payload["backend_code"] = exc.backend_code
|
|
15512
16180
|
if exc.http_status is not None:
|
|
@@ -15516,11 +16184,35 @@ def _record_detail_unavailable_context(section: str, message: str, exc: Qingflow
|
|
|
15516
16184
|
payload["request_id"] = request_id
|
|
15517
16185
|
details = exc.details if isinstance(exc.details, dict) else {}
|
|
15518
16186
|
error_code = details.get("error_code")
|
|
15519
|
-
if error_code:
|
|
16187
|
+
if error_code and not payload.get("error_code"):
|
|
15520
16188
|
payload["error_code"] = error_code
|
|
15521
16189
|
return payload
|
|
15522
16190
|
|
|
15523
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
|
+
|
|
15524
16216
|
_RECORD_MEDIA_IMG_SRC_RE = re.compile(r"""<img\b[^>]*\bsrc\s*=\s*["']?([^"'\s>]+)""", re.IGNORECASE)
|
|
15525
16217
|
_RECORD_MEDIA_MD_IMAGE_RE = re.compile(r"""!\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)""")
|
|
15526
16218
|
_RECORD_MEDIA_URL_RE = re.compile(r"""https?://[^\s<>"')\]]+""", re.IGNORECASE)
|
|
@@ -15675,7 +16367,14 @@ def _record_detail_media_assets_payload(
|
|
|
15675
16367
|
except QingflowApiError as exc:
|
|
15676
16368
|
blocked = exc.http_status in {401, 403}
|
|
15677
16369
|
if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
|
|
15678
|
-
|
|
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)
|
|
15679
16378
|
if refreshed_url and refreshed_url != source_url:
|
|
15680
16379
|
refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
|
|
15681
16380
|
try:
|
|
@@ -15717,14 +16416,13 @@ def _record_detail_media_assets_payload(
|
|
|
15717
16416
|
"readable_by_agent": False,
|
|
15718
16417
|
}
|
|
15719
16418
|
)
|
|
15720
|
-
|
|
15721
|
-
|
|
15722
|
-
|
|
15723
|
-
|
|
15724
|
-
|
|
15725
|
-
|
|
15726
|
-
|
|
15727
|
-
)
|
|
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)
|
|
15728
16426
|
continue
|
|
15729
16427
|
|
|
15730
16428
|
if not isinstance(content, bytes):
|
|
@@ -15942,7 +16640,14 @@ def _record_detail_file_assets_payload(
|
|
|
15942
16640
|
except QingflowApiError as exc:
|
|
15943
16641
|
blocked = exc.http_status in {401, 403}
|
|
15944
16642
|
if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
|
|
15945
|
-
|
|
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)
|
|
15946
16651
|
if refreshed_url and refreshed_url != source_url:
|
|
15947
16652
|
refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
|
|
15948
16653
|
try:
|
|
@@ -15989,14 +16694,13 @@ def _record_detail_file_assets_payload(
|
|
|
15989
16694
|
"extraction": {"status": "failed", "text_path": None, "preview": None},
|
|
15990
16695
|
}
|
|
15991
16696
|
)
|
|
15992
|
-
|
|
15993
|
-
|
|
15994
|
-
|
|
15995
|
-
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
)
|
|
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)
|
|
16000
16704
|
continue
|
|
16001
16705
|
|
|
16002
16706
|
if not isinstance(content, bytes):
|