@qingflow-tech/qingflow-app-user-mcp 1.0.11 → 1.0.13
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 +9 -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 +255 -0
- package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder/references/create-app.md +149 -0
- package/skills/qingflow-app-builder/references/environments.md +63 -0
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +107 -0
- package/skills/qingflow-app-builder/references/match-rules.md +114 -0
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
- package/skills/qingflow-app-builder/references/update-flow.md +158 -0
- package/skills/qingflow-app-builder/references/update-layout.md +68 -0
- package/skills/qingflow-app-builder/references/update-schema.md +72 -0
- package/skills/qingflow-app-builder/references/update-views.md +284 -0
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/skills/qingflow-app-user/SKILL.md +12 -11
- package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
- package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
- package/skills/qingflow-app-user/references/record-patterns.md +5 -5
- package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
- package/skills/qingflow-mcp-setup/SKILL.md +113 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +62 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-record-analysis/SKILL.md +6 -7
- package/skills/qingflow-record-analysis/manifest.yaml +10 -0
- package/skills/qingflow-record-delete/SKILL.md +5 -3
- package/skills/qingflow-record-import/SKILL.md +6 -2
- package/skills/qingflow-record-insert/SKILL.md +48 -4
- package/skills/qingflow-record-insert/manifest.yaml +6 -0
- package/skills/qingflow-record-update/SKILL.md +36 -24
- package/skills/qingflow-task-ops/SKILL.md +25 -25
- package/skills/qingflow-task-ops/references/environments.md +0 -1
- package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +11 -0
- 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 +91 -19
- 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 +74 -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 +19 -13
- package/src/qingflow_mcp/session_store.py +11 -7
- 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 +298 -40
- 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 +244 -34
- package/src/qingflow_mcp/tools/file_tools.py +7 -3
- package/src/qingflow_mcp/tools/import_tools.py +336 -49
- 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 +1067 -349
- 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
|
|
@@ -322,11 +350,12 @@ class RecordTools(ToolBase):
|
|
|
322
350
|
"""注册当前工具到 MCP 服务。"""
|
|
323
351
|
@mcp.tool()
|
|
324
352
|
def record_insert_schema_get(
|
|
353
|
+
profile: str = DEFAULT_PROFILE,
|
|
325
354
|
app_key: str = "",
|
|
326
355
|
output_profile: str = "normal",
|
|
327
356
|
) -> JSONObject:
|
|
328
357
|
return self.record_insert_schema_get_public(
|
|
329
|
-
profile=
|
|
358
|
+
profile=profile,
|
|
330
359
|
app_key=app_key,
|
|
331
360
|
output_profile=output_profile,
|
|
332
361
|
)
|
|
@@ -436,6 +465,7 @@ class RecordTools(ToolBase):
|
|
|
436
465
|
)
|
|
437
466
|
)
|
|
438
467
|
def record_access(
|
|
468
|
+
profile: str = DEFAULT_PROFILE,
|
|
439
469
|
app_key: str = "",
|
|
440
470
|
view_id: str = "",
|
|
441
471
|
columns: list[JSONObject | int] | None = None,
|
|
@@ -443,7 +473,7 @@ class RecordTools(ToolBase):
|
|
|
443
473
|
order_by: list[JSONObject] | None = None,
|
|
444
474
|
) -> JSONObject:
|
|
445
475
|
return self.record_access(
|
|
446
|
-
profile=
|
|
476
|
+
profile=profile,
|
|
447
477
|
app_key=app_key,
|
|
448
478
|
view_id=view_id,
|
|
449
479
|
columns=columns or [],
|
|
@@ -471,7 +501,7 @@ class RecordTools(ToolBase):
|
|
|
471
501
|
output_profile=output_profile,
|
|
472
502
|
)
|
|
473
503
|
|
|
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.")
|
|
504
|
+
@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
505
|
def record_logs_get(
|
|
476
506
|
profile: str = DEFAULT_PROFILE,
|
|
477
507
|
app_key: str = "",
|
|
@@ -487,12 +517,13 @@ class RecordTools(ToolBase):
|
|
|
487
517
|
|
|
488
518
|
@mcp.tool()
|
|
489
519
|
def record_browse_schema_get(
|
|
520
|
+
profile: str = DEFAULT_PROFILE,
|
|
490
521
|
app_key: str = "",
|
|
491
522
|
view_id: str = "",
|
|
492
523
|
output_profile: str = "normal",
|
|
493
524
|
) -> JSONObject:
|
|
494
525
|
return self.record_browse_schema_get_public(
|
|
495
|
-
profile=
|
|
526
|
+
profile=profile,
|
|
496
527
|
app_key=app_key,
|
|
497
528
|
view_id=view_id,
|
|
498
529
|
output_profile=output_profile,
|
|
@@ -500,14 +531,17 @@ class RecordTools(ToolBase):
|
|
|
500
531
|
|
|
501
532
|
@mcp.tool()
|
|
502
533
|
def record_update_schema_get(
|
|
534
|
+
profile: str = DEFAULT_PROFILE,
|
|
503
535
|
app_key: str = "",
|
|
504
536
|
record_id: str = "",
|
|
537
|
+
view_id: str | None = None,
|
|
505
538
|
output_profile: str = "normal",
|
|
506
539
|
) -> JSONObject:
|
|
507
540
|
return self.record_update_schema_get_public(
|
|
508
|
-
profile=
|
|
541
|
+
profile=profile,
|
|
509
542
|
app_key=app_key,
|
|
510
543
|
record_id=record_id,
|
|
544
|
+
view_id=view_id,
|
|
511
545
|
output_profile=output_profile,
|
|
512
546
|
)
|
|
513
547
|
|
|
@@ -520,13 +554,14 @@ class RecordTools(ToolBase):
|
|
|
520
554
|
)
|
|
521
555
|
)
|
|
522
556
|
def record_insert(
|
|
557
|
+
profile: str = DEFAULT_PROFILE,
|
|
523
558
|
app_key: str = "",
|
|
524
559
|
items: list[JSONObject] | None = None,
|
|
525
560
|
verify_write: bool = True,
|
|
526
561
|
output_profile: str = "normal",
|
|
527
562
|
) -> JSONObject:
|
|
528
563
|
return self.record_insert_public(
|
|
529
|
-
profile=
|
|
564
|
+
profile=profile,
|
|
530
565
|
app_key=app_key,
|
|
531
566
|
items=items,
|
|
532
567
|
verify_write=verify_write,
|
|
@@ -537,25 +572,29 @@ class RecordTools(ToolBase):
|
|
|
537
572
|
description=(
|
|
538
573
|
"Update one Qingflow record using a field map. "
|
|
539
574
|
"For simple field changes, call this tool directly after the target record is clear. "
|
|
575
|
+
"Pass view_id when the frontend detail view is known; the tool will try that view first. "
|
|
540
576
|
"It first tries the data-manager direct route, then falls back to the frontend custom-view edit route when available. "
|
|
541
577
|
"Use record_update_schema_get for diagnostics or complex field-scope inspection."
|
|
542
578
|
)
|
|
543
579
|
)
|
|
544
580
|
def record_update(
|
|
581
|
+
profile: str = DEFAULT_PROFILE,
|
|
545
582
|
app_key: str = "",
|
|
546
583
|
record_id: str | None = None,
|
|
547
584
|
fields: JSONObject | None = None,
|
|
548
585
|
items: list[JSONObject] | None = None,
|
|
586
|
+
view_id: str | None = None,
|
|
549
587
|
dry_run: bool = False,
|
|
550
588
|
verify_write: bool = True,
|
|
551
589
|
output_profile: str = "normal",
|
|
552
590
|
) -> JSONObject:
|
|
553
591
|
return self.record_update_public(
|
|
554
|
-
profile=
|
|
592
|
+
profile=profile,
|
|
555
593
|
app_key=app_key,
|
|
556
594
|
record_id=record_id,
|
|
557
595
|
fields=fields,
|
|
558
596
|
items=items,
|
|
597
|
+
view_id=view_id,
|
|
559
598
|
dry_run=dry_run,
|
|
560
599
|
verify_write=verify_write,
|
|
561
600
|
output_profile=output_profile,
|
|
@@ -564,20 +603,23 @@ class RecordTools(ToolBase):
|
|
|
564
603
|
@mcp.tool(
|
|
565
604
|
description=(
|
|
566
605
|
"Delete Qingflow records by record_id or record_ids. "
|
|
567
|
-
"
|
|
606
|
+
"Pass view_id when deleting from a known system list route; custom view deletion is not supported by this backend route."
|
|
568
607
|
)
|
|
569
608
|
)
|
|
570
609
|
def record_delete(
|
|
610
|
+
profile: str = DEFAULT_PROFILE,
|
|
571
611
|
app_key: str = "",
|
|
572
612
|
record_id: str | None = None,
|
|
573
613
|
record_ids: list[str] | None = None,
|
|
614
|
+
view_id: str | None = None,
|
|
574
615
|
output_profile: str = "normal",
|
|
575
616
|
) -> JSONObject:
|
|
576
617
|
return self.record_delete_public(
|
|
577
|
-
profile=
|
|
618
|
+
profile=profile,
|
|
578
619
|
app_key=app_key,
|
|
579
620
|
record_id=record_id,
|
|
580
621
|
record_ids=record_ids or [],
|
|
622
|
+
view_id=view_id,
|
|
581
623
|
output_profile=output_profile,
|
|
582
624
|
)
|
|
583
625
|
|
|
@@ -849,6 +891,7 @@ class RecordTools(ToolBase):
|
|
|
849
891
|
profile: str = DEFAULT_PROFILE,
|
|
850
892
|
app_key: str,
|
|
851
893
|
record_id: Any,
|
|
894
|
+
view_id: str | None = None,
|
|
852
895
|
output_profile: str = "normal",
|
|
853
896
|
) -> JSONObject:
|
|
854
897
|
"""执行记录相关逻辑。"""
|
|
@@ -860,21 +903,44 @@ class RecordTools(ToolBase):
|
|
|
860
903
|
def runner(session_profile, context):
|
|
861
904
|
request_route = self._request_route_payload(context)
|
|
862
905
|
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
|
-
|
|
906
|
+
linkage_payloads_by_field_id: dict[str, JSONObject] = {}
|
|
907
|
+
try:
|
|
908
|
+
app_schema = self._get_form_schema(profile, context, app_key, force_refresh=True)
|
|
909
|
+
except QingflowApiError as exc:
|
|
910
|
+
if not _is_optional_schema_permission_error(exc):
|
|
911
|
+
raise
|
|
912
|
+
else:
|
|
913
|
+
question_relations = _collect_question_relations(app_schema)
|
|
914
|
+
app_index = _build_applicant_top_level_field_index(app_schema)
|
|
915
|
+
linked_field_ids = _collect_linked_required_field_ids(question_relations)
|
|
916
|
+
linked_field_ids.update(_collect_option_linked_field_ids(app_index))
|
|
917
|
+
linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
|
|
918
|
+
app_schema,
|
|
919
|
+
linked_field_ids=linked_field_ids,
|
|
920
|
+
)
|
|
921
|
+
app_index = _merge_field_indexes(app_index, linked_hidden_index)
|
|
922
|
+
linkage_payloads_by_field_id = _build_static_schema_linkage_payloads(
|
|
923
|
+
index=app_index,
|
|
924
|
+
question_relations=question_relations,
|
|
925
|
+
)
|
|
926
|
+
preferred_view_id = _normalize_optional_text(view_id)
|
|
877
927
|
candidate_routes = self._candidate_update_views(profile, context, app_key)
|
|
928
|
+
if preferred_view_id:
|
|
929
|
+
preferred_route = next(
|
|
930
|
+
(
|
|
931
|
+
route
|
|
932
|
+
for route in candidate_routes
|
|
933
|
+
if route.view_id == preferred_view_id
|
|
934
|
+
),
|
|
935
|
+
None,
|
|
936
|
+
)
|
|
937
|
+
if preferred_route is None:
|
|
938
|
+
raise_tool_error(
|
|
939
|
+
QingflowApiError.config_error(
|
|
940
|
+
f"view_id '{preferred_view_id}' is not an accessible update candidate"
|
|
941
|
+
)
|
|
942
|
+
)
|
|
943
|
+
candidate_routes = [preferred_route]
|
|
878
944
|
probes = self._probe_candidate_record_contexts(
|
|
879
945
|
context,
|
|
880
946
|
app_key=app_key,
|
|
@@ -976,6 +1042,7 @@ class RecordTools(ToolBase):
|
|
|
976
1042
|
output_profile=normalized_output_profile,
|
|
977
1043
|
view_probe_summary=probe_summary,
|
|
978
1044
|
ambiguous_fields=[],
|
|
1045
|
+
preferred_view_id=preferred_view_id,
|
|
979
1046
|
)
|
|
980
1047
|
|
|
981
1048
|
ambiguous_field_ids: set[int] = set()
|
|
@@ -1022,6 +1089,7 @@ class RecordTools(ToolBase):
|
|
|
1022
1089
|
output_profile=normalized_output_profile,
|
|
1023
1090
|
view_probe_summary=probe_summary,
|
|
1024
1091
|
ambiguous_fields=ambiguous_fields,
|
|
1092
|
+
preferred_view_id=preferred_view_id,
|
|
1025
1093
|
)
|
|
1026
1094
|
|
|
1027
1095
|
response: JSONObject = {
|
|
@@ -1046,6 +1114,8 @@ class RecordTools(ToolBase):
|
|
|
1046
1114
|
"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
1115
|
},
|
|
1048
1116
|
}
|
|
1117
|
+
if preferred_view_id:
|
|
1118
|
+
response["preferred_view_id"] = preferred_view_id
|
|
1049
1119
|
if normalized_output_profile == "verbose":
|
|
1050
1120
|
response["view_probe_summary"] = probe_summary
|
|
1051
1121
|
response["record_context_probe"] = probe_summary
|
|
@@ -1106,6 +1176,7 @@ class RecordTools(ToolBase):
|
|
|
1106
1176
|
output_profile: str,
|
|
1107
1177
|
view_probe_summary: list[JSONObject],
|
|
1108
1178
|
ambiguous_fields: list[JSONObject],
|
|
1179
|
+
preferred_view_id: str | None = None,
|
|
1109
1180
|
) -> JSONObject:
|
|
1110
1181
|
"""执行内部辅助逻辑。"""
|
|
1111
1182
|
response: JSONObject = {
|
|
@@ -1123,6 +1194,8 @@ class RecordTools(ToolBase):
|
|
|
1123
1194
|
"payload_template": {},
|
|
1124
1195
|
"recommended_next_actions": recommended_next_actions,
|
|
1125
1196
|
}
|
|
1197
|
+
if preferred_view_id:
|
|
1198
|
+
response["preferred_view_id"] = preferred_view_id
|
|
1126
1199
|
if output_profile == "verbose":
|
|
1127
1200
|
response["view_probe_summary"] = view_probe_summary
|
|
1128
1201
|
response["ambiguous_fields"] = ambiguous_fields
|
|
@@ -1440,24 +1513,58 @@ class RecordTools(ToolBase):
|
|
|
1440
1513
|
)
|
|
1441
1514
|
warnings: list[JSONObject] = []
|
|
1442
1515
|
scope_source = "static_applicant_scope"
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1516
|
+
try:
|
|
1517
|
+
if runtime_lookup:
|
|
1518
|
+
state = self._build_candidate_lookup_state(
|
|
1519
|
+
profile,
|
|
1520
|
+
context,
|
|
1521
|
+
app_key=app_key,
|
|
1522
|
+
record_id=record_id_int,
|
|
1523
|
+
workflow_node_id=workflow_node_id,
|
|
1524
|
+
fields=normalized_fields,
|
|
1525
|
+
)
|
|
1526
|
+
items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1527
|
+
scope_source = "backend_runtime_scope"
|
|
1528
|
+
else:
|
|
1529
|
+
items: list[JSONObject] | None = None
|
|
1530
|
+
if self._member_candidate_static_preview_should_use_backend(field):
|
|
1531
|
+
state = self._build_candidate_lookup_state(
|
|
1532
|
+
profile,
|
|
1533
|
+
context,
|
|
1534
|
+
app_key=app_key,
|
|
1535
|
+
record_id=None,
|
|
1536
|
+
workflow_node_id=None,
|
|
1537
|
+
fields={},
|
|
1538
|
+
)
|
|
1539
|
+
items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1540
|
+
scope_source = "backend_applicant_scope"
|
|
1541
|
+
if items is None:
|
|
1542
|
+
items = self._resolve_member_candidates(context, field, keyword=keyword)
|
|
1543
|
+
warnings.append(
|
|
1544
|
+
{
|
|
1545
|
+
"code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
|
|
1546
|
+
"message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
|
|
1547
|
+
}
|
|
1548
|
+
)
|
|
1549
|
+
except (RecordInputError, QingflowApiError) as error:
|
|
1550
|
+
record_error = (
|
|
1551
|
+
error
|
|
1552
|
+
if isinstance(error, RecordInputError)
|
|
1553
|
+
else self._candidate_lookup_error(kind="member", field=field, value=keyword, error=error)
|
|
1554
|
+
)
|
|
1555
|
+
return self._candidate_lookup_failed_response(
|
|
1556
|
+
profile=profile,
|
|
1557
|
+
session_profile=session_profile,
|
|
1558
|
+
context=context,
|
|
1559
|
+
kind="member",
|
|
1560
|
+
error=record_error,
|
|
1561
|
+
field=field,
|
|
1447
1562
|
app_key=app_key,
|
|
1448
|
-
|
|
1563
|
+
record_id_text=record_id_text,
|
|
1449
1564
|
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
|
-
}
|
|
1565
|
+
fields_present=bool(normalized_fields),
|
|
1566
|
+
keyword=keyword,
|
|
1567
|
+
scope_source=scope_source,
|
|
1461
1568
|
)
|
|
1462
1569
|
total = len(items)
|
|
1463
1570
|
start = (page_num - 1) * page_size
|
|
@@ -1550,41 +1657,75 @@ class RecordTools(ToolBase):
|
|
|
1550
1657
|
)
|
|
1551
1658
|
warnings: list[JSONObject] = []
|
|
1552
1659
|
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
|
-
):
|
|
1660
|
+
try:
|
|
1661
|
+
if runtime_lookup:
|
|
1573
1662
|
state = self._build_candidate_lookup_state(
|
|
1574
1663
|
profile,
|
|
1575
1664
|
context,
|
|
1576
1665
|
app_key=app_key,
|
|
1577
|
-
record_id=
|
|
1578
|
-
workflow_node_id=
|
|
1579
|
-
fields=
|
|
1666
|
+
record_id=record_id_int,
|
|
1667
|
+
workflow_node_id=workflow_node_id,
|
|
1668
|
+
fields=normalized_fields,
|
|
1580
1669
|
)
|
|
1581
1670
|
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1582
1671
|
scope_source = "backend_runtime_scope"
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1672
|
+
else:
|
|
1673
|
+
items: list[JSONObject] | None = None
|
|
1674
|
+
if self._department_candidate_static_preview_should_use_backend(field):
|
|
1675
|
+
state = self._build_candidate_lookup_state(
|
|
1676
|
+
profile,
|
|
1677
|
+
context,
|
|
1678
|
+
app_key=app_key,
|
|
1679
|
+
record_id=None,
|
|
1680
|
+
workflow_node_id=None,
|
|
1681
|
+
fields={},
|
|
1682
|
+
)
|
|
1683
|
+
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1684
|
+
scope_source = "backend_applicant_scope"
|
|
1685
|
+
if items is None:
|
|
1686
|
+
items = self._resolve_department_candidates(context, field, keyword=keyword)
|
|
1687
|
+
scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
|
|
1688
|
+
if (
|
|
1689
|
+
not items
|
|
1690
|
+
and field.dept_select_scope_type == 2
|
|
1691
|
+
and not _scope_has_dynamic_or_external(scope)
|
|
1692
|
+
and not list(scope.get("depart") or [])
|
|
1693
|
+
):
|
|
1694
|
+
state = self._build_candidate_lookup_state(
|
|
1695
|
+
profile,
|
|
1696
|
+
context,
|
|
1697
|
+
app_key=app_key,
|
|
1698
|
+
record_id=None,
|
|
1699
|
+
workflow_node_id=None,
|
|
1700
|
+
fields={},
|
|
1701
|
+
)
|
|
1702
|
+
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1703
|
+
scope_source = "backend_applicant_scope"
|
|
1704
|
+
warnings.append(
|
|
1705
|
+
{
|
|
1706
|
+
"code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
|
|
1707
|
+
"message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
|
|
1708
|
+
}
|
|
1709
|
+
)
|
|
1710
|
+
except (RecordInputError, QingflowApiError) as error:
|
|
1711
|
+
record_error = (
|
|
1712
|
+
error
|
|
1713
|
+
if isinstance(error, RecordInputError)
|
|
1714
|
+
else self._candidate_lookup_error(kind="department", field=field, value=keyword, error=error)
|
|
1715
|
+
)
|
|
1716
|
+
return self._candidate_lookup_failed_response(
|
|
1717
|
+
profile=profile,
|
|
1718
|
+
session_profile=session_profile,
|
|
1719
|
+
context=context,
|
|
1720
|
+
kind="department",
|
|
1721
|
+
error=record_error,
|
|
1722
|
+
field=field,
|
|
1723
|
+
app_key=app_key,
|
|
1724
|
+
record_id_text=record_id_text,
|
|
1725
|
+
workflow_node_id=workflow_node_id,
|
|
1726
|
+
fields_present=bool(normalized_fields),
|
|
1727
|
+
keyword=keyword,
|
|
1728
|
+
scope_source=scope_source,
|
|
1588
1729
|
)
|
|
1589
1730
|
total = len(items)
|
|
1590
1731
|
start = (page_num - 1) * page_size
|
|
@@ -1646,6 +1787,21 @@ class RecordTools(ToolBase):
|
|
|
1646
1787
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
1647
1788
|
if limit <= 0:
|
|
1648
1789
|
raise_tool_error(QingflowApiError.config_error("limit must be positive"))
|
|
1790
|
+
if not (
|
|
1791
|
+
_normalize_optional_text(view_id)
|
|
1792
|
+
or list_type is not None
|
|
1793
|
+
or _normalize_optional_text(view_key)
|
|
1794
|
+
or _normalize_optional_text(view_name)
|
|
1795
|
+
):
|
|
1796
|
+
raise_tool_error(
|
|
1797
|
+
QingflowApiError.config_error(
|
|
1798
|
+
"record_analyze requires view_id. Call app_get first and pass accessible_views[].view_id.",
|
|
1799
|
+
details={
|
|
1800
|
+
"error_code": "RECORD_ANALYZE_VIEW_REQUIRED",
|
|
1801
|
+
"fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_analyze with view_id.",
|
|
1802
|
+
},
|
|
1803
|
+
)
|
|
1804
|
+
)
|
|
1649
1805
|
legacy_warnings = _detect_analyze_legacy_warnings(
|
|
1650
1806
|
dimensions=dimensions,
|
|
1651
1807
|
metrics=metrics,
|
|
@@ -1662,7 +1818,7 @@ class RecordTools(ToolBase):
|
|
|
1662
1818
|
list_type=list_type,
|
|
1663
1819
|
view_key=view_key,
|
|
1664
1820
|
view_name=view_name,
|
|
1665
|
-
allow_default=
|
|
1821
|
+
allow_default=False,
|
|
1666
1822
|
)
|
|
1667
1823
|
if not _view_type_supports_analysis(resolved_view.view_type):
|
|
1668
1824
|
raise_tool_error(
|
|
@@ -1743,6 +1899,21 @@ class RecordTools(ToolBase):
|
|
|
1743
1899
|
raise_tool_error(QingflowApiError.config_error("limit must be positive"))
|
|
1744
1900
|
if page <= 0:
|
|
1745
1901
|
raise_tool_error(QingflowApiError.config_error("page must be positive"))
|
|
1902
|
+
if not (
|
|
1903
|
+
_normalize_optional_text(view_id)
|
|
1904
|
+
or list_type is not None
|
|
1905
|
+
or _normalize_optional_text(view_key)
|
|
1906
|
+
or _normalize_optional_text(view_name)
|
|
1907
|
+
):
|
|
1908
|
+
raise_tool_error(
|
|
1909
|
+
QingflowApiError.config_error(
|
|
1910
|
+
"record_list requires view_id. Call app_get first and pass accessible_views[].view_id.",
|
|
1911
|
+
details={
|
|
1912
|
+
"error_code": "RECORD_LIST_VIEW_REQUIRED",
|
|
1913
|
+
"fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_list with view_id.",
|
|
1914
|
+
},
|
|
1915
|
+
)
|
|
1916
|
+
)
|
|
1746
1917
|
view_route, compatibility_warnings = self._resolve_accessible_view_route_for_public(
|
|
1747
1918
|
profile=profile,
|
|
1748
1919
|
app_key=app_key,
|
|
@@ -1750,7 +1921,7 @@ class RecordTools(ToolBase):
|
|
|
1750
1921
|
list_type=list_type,
|
|
1751
1922
|
view_key=view_key,
|
|
1752
1923
|
view_name=view_name,
|
|
1753
|
-
allow_default=
|
|
1924
|
+
allow_default=False,
|
|
1754
1925
|
)
|
|
1755
1926
|
if not _view_type_supports_analysis(view_route.view_type):
|
|
1756
1927
|
raise_tool_error(
|
|
@@ -2224,6 +2395,7 @@ class RecordTools(ToolBase):
|
|
|
2224
2395
|
requested_output_profile = _normalize_record_get_detail_output_profile(output_profile)
|
|
2225
2396
|
record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
|
|
2226
2397
|
normalized_columns = _normalize_public_column_selectors(columns)
|
|
2398
|
+
explicit_view_id = _normalize_optional_text(view_id)
|
|
2227
2399
|
|
|
2228
2400
|
def runner(session_profile, context):
|
|
2229
2401
|
resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
|
|
@@ -2249,17 +2421,45 @@ class RecordTools(ToolBase):
|
|
|
2249
2421
|
"code": "OUTPUT_PROFILE_DEPRECATED_FOR_DETAIL_CONTEXT",
|
|
2250
2422
|
"message": f"output_profile={requested_output_profile!r} is deprecated for record_get; detail_context is always returned.",
|
|
2251
2423
|
})
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2424
|
+
def get_detail_for_route(route: AccessibleViewRoute, route_warnings: list[JSONObject]) -> JSONObject:
|
|
2425
|
+
return self._record_get_detail_context(
|
|
2426
|
+
profile=profile,
|
|
2427
|
+
session_profile=session_profile,
|
|
2428
|
+
context=context,
|
|
2429
|
+
app_key=app_key,
|
|
2430
|
+
record_id_int=record_id_int,
|
|
2431
|
+
resolved_view=route,
|
|
2432
|
+
requested_focus_field_ids=normalized_columns,
|
|
2433
|
+
workflow_node_id=workflow_node_id,
|
|
2434
|
+
warnings=route_warnings,
|
|
2435
|
+
)
|
|
2436
|
+
|
|
2437
|
+
try:
|
|
2438
|
+
return get_detail_for_route(resolved_view, warnings)
|
|
2439
|
+
except QingflowApiError as exc:
|
|
2440
|
+
if explicit_view_id is not None:
|
|
2441
|
+
raise
|
|
2442
|
+
if not self._is_record_context_route_miss(exc):
|
|
2443
|
+
raise
|
|
2444
|
+
fallback_warnings = list(warnings)
|
|
2445
|
+
fallback_warnings.append(
|
|
2446
|
+
{
|
|
2447
|
+
"code": "DEFAULT_DETAIL_ROUTE_DENIED",
|
|
2448
|
+
"message": "record_get default system:all route was not readable; trying accessible views that match the frontend route model.",
|
|
2449
|
+
"backend_code": exc.backend_code,
|
|
2450
|
+
}
|
|
2451
|
+
)
|
|
2452
|
+
last_error = exc
|
|
2453
|
+
for candidate in self._candidate_update_views(profile, context, app_key):
|
|
2454
|
+
if candidate.view_id == resolved_view.view_id:
|
|
2455
|
+
continue
|
|
2456
|
+
try:
|
|
2457
|
+
return get_detail_for_route(candidate, fallback_warnings)
|
|
2458
|
+
except QingflowApiError as candidate_exc:
|
|
2459
|
+
if not self._is_record_context_route_miss(candidate_exc):
|
|
2460
|
+
raise
|
|
2461
|
+
last_error = candidate_exc
|
|
2462
|
+
raise last_error
|
|
2263
2463
|
|
|
2264
2464
|
return self._run_record_tool(profile, runner)
|
|
2265
2465
|
|
|
@@ -2274,6 +2474,16 @@ class RecordTools(ToolBase):
|
|
|
2274
2474
|
) -> JSONObject:
|
|
2275
2475
|
"""读取单条记录可见的全量数据日志和流程日志,写入本地 JSONL。"""
|
|
2276
2476
|
record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
|
|
2477
|
+
if not _normalize_optional_text(view_id):
|
|
2478
|
+
raise_tool_error(
|
|
2479
|
+
QingflowApiError.config_error(
|
|
2480
|
+
"record_logs_get requires view_id. Call app_get first and pass accessible_views[].view_id.",
|
|
2481
|
+
details={
|
|
2482
|
+
"error_code": "RECORD_LOGS_VIEW_REQUIRED",
|
|
2483
|
+
"fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_logs_get with view_id.",
|
|
2484
|
+
},
|
|
2485
|
+
)
|
|
2486
|
+
)
|
|
2277
2487
|
|
|
2278
2488
|
def runner(session_profile, context):
|
|
2279
2489
|
resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
|
|
@@ -2284,21 +2494,45 @@ class RecordTools(ToolBase):
|
|
|
2284
2494
|
list_type=None,
|
|
2285
2495
|
view_key=None,
|
|
2286
2496
|
view_name=None,
|
|
2287
|
-
allow_default=
|
|
2497
|
+
allow_default=False,
|
|
2288
2498
|
)
|
|
2289
2499
|
warnings: list[JSONObject] = []
|
|
2290
2500
|
warnings.extend(compatibility_warnings)
|
|
2291
2501
|
warnings.extend(_view_filter_trust_warnings(resolved_view))
|
|
2292
2502
|
unavailable_context: list[JSONObject] = []
|
|
2293
2503
|
|
|
2294
|
-
schema =
|
|
2504
|
+
schema: JSONObject = {}
|
|
2505
|
+
try:
|
|
2506
|
+
schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
|
|
2507
|
+
except QingflowApiError as exc:
|
|
2508
|
+
if not _is_optional_schema_permission_error(exc):
|
|
2509
|
+
raise
|
|
2510
|
+
unavailable_context.append(
|
|
2511
|
+
_record_detail_unavailable_context(
|
|
2512
|
+
"detail_schema",
|
|
2513
|
+
"记录日志字段结构辅助信息获取失败,已尝试使用详情主数据中的字段信息继续。",
|
|
2514
|
+
exc,
|
|
2515
|
+
)
|
|
2516
|
+
)
|
|
2295
2517
|
index = _build_top_level_field_index(schema)
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2518
|
+
try:
|
|
2519
|
+
audit_info = self._record_get_audit_info(
|
|
2520
|
+
context,
|
|
2521
|
+
app_key=app_key,
|
|
2522
|
+
record_id=record_id_int,
|
|
2523
|
+
resolved_view=resolved_view,
|
|
2524
|
+
)
|
|
2525
|
+
except QingflowApiError as exc:
|
|
2526
|
+
if not _is_optional_schema_permission_error(exc):
|
|
2527
|
+
raise
|
|
2528
|
+
audit_info = {}
|
|
2529
|
+
unavailable_context.append(
|
|
2530
|
+
_record_detail_unavailable_context(
|
|
2531
|
+
"audit_info",
|
|
2532
|
+
"记录审批节点辅助信息获取失败,已继续读取详情主数据和日志。",
|
|
2533
|
+
exc,
|
|
2534
|
+
)
|
|
2535
|
+
)
|
|
2302
2536
|
audit_context = _record_detail_audit_context(audit_info, workflow_node_id=None)
|
|
2303
2537
|
detail_result, used_list_type, used_role = self._record_get_apply_detail(
|
|
2304
2538
|
context,
|
|
@@ -2308,6 +2542,17 @@ class RecordTools(ToolBase):
|
|
|
2308
2542
|
audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
|
|
2309
2543
|
)
|
|
2310
2544
|
answer_list = _record_detail_answers(detail_result)
|
|
2545
|
+
if not index.by_id:
|
|
2546
|
+
answer_index = _build_answer_backed_field_index(cast(list[JSONObject], answer_list))
|
|
2547
|
+
if answer_index.by_id:
|
|
2548
|
+
index = answer_index
|
|
2549
|
+
unavailable_context.append(
|
|
2550
|
+
{
|
|
2551
|
+
"section": "detail_schema",
|
|
2552
|
+
"message": "字段结构由详情 answers 回退构造;候选范围、只读状态、选项和联动信息可能不完整。",
|
|
2553
|
+
"category": "partial_context",
|
|
2554
|
+
}
|
|
2555
|
+
)
|
|
2311
2556
|
selected_fields = list(index.by_id.values())
|
|
2312
2557
|
fields = [
|
|
2313
2558
|
_record_detail_field_payload(field, _find_answer_for_field(cast(list[JSONValue], answer_list), field), focus_id_set=set())
|
|
@@ -2401,14 +2646,41 @@ class RecordTools(ToolBase):
|
|
|
2401
2646
|
warnings: list[JSONObject],
|
|
2402
2647
|
) -> JSONObject:
|
|
2403
2648
|
"""执行内部辅助逻辑。"""
|
|
2404
|
-
|
|
2649
|
+
unavailable_context: list[JSONObject] = []
|
|
2650
|
+
schema: JSONObject = {}
|
|
2651
|
+
schema_available = True
|
|
2652
|
+
try:
|
|
2653
|
+
schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
|
|
2654
|
+
except QingflowApiError as exc:
|
|
2655
|
+
if not _is_optional_schema_permission_error(exc):
|
|
2656
|
+
raise
|
|
2657
|
+
schema_available = False
|
|
2658
|
+
unavailable_context.append(
|
|
2659
|
+
_record_detail_unavailable_context(
|
|
2660
|
+
"detail_schema",
|
|
2661
|
+
"记录详情字段结构辅助信息获取失败,已尝试使用详情主数据中的字段信息继续。",
|
|
2662
|
+
exc,
|
|
2663
|
+
)
|
|
2664
|
+
)
|
|
2405
2665
|
index = _build_top_level_field_index(schema)
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2666
|
+
try:
|
|
2667
|
+
audit_info = self._record_get_audit_info(
|
|
2668
|
+
context,
|
|
2669
|
+
app_key=app_key,
|
|
2670
|
+
record_id=record_id_int,
|
|
2671
|
+
resolved_view=resolved_view,
|
|
2672
|
+
)
|
|
2673
|
+
except QingflowApiError as exc:
|
|
2674
|
+
if not _is_optional_schema_permission_error(exc):
|
|
2675
|
+
raise
|
|
2676
|
+
audit_info = {}
|
|
2677
|
+
unavailable_context.append(
|
|
2678
|
+
_record_detail_unavailable_context(
|
|
2679
|
+
"audit_info",
|
|
2680
|
+
"记录审批节点辅助信息获取失败,已继续读取详情主数据。",
|
|
2681
|
+
exc,
|
|
2682
|
+
)
|
|
2683
|
+
)
|
|
2412
2684
|
audit_context = _record_detail_audit_context(audit_info, workflow_node_id=workflow_node_id)
|
|
2413
2685
|
detail_result, used_list_type, used_role = self._record_get_apply_detail(
|
|
2414
2686
|
context,
|
|
@@ -2418,13 +2690,24 @@ class RecordTools(ToolBase):
|
|
|
2418
2690
|
audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
|
|
2419
2691
|
)
|
|
2420
2692
|
answer_list = _record_detail_answers(detail_result)
|
|
2693
|
+
if not index.by_id:
|
|
2694
|
+
answer_index = _build_answer_backed_field_index(cast(list[JSONObject], answer_list))
|
|
2695
|
+
if answer_index.by_id:
|
|
2696
|
+
index = answer_index
|
|
2697
|
+
unavailable_context.append(
|
|
2698
|
+
{
|
|
2699
|
+
"section": "detail_schema",
|
|
2700
|
+
"message": "字段结构由详情 answers 回退构造;候选范围、只读状态、选项和联动信息可能不完整。",
|
|
2701
|
+
"category": "partial_context",
|
|
2702
|
+
}
|
|
2703
|
+
)
|
|
2421
2704
|
selected_fields = list(index.by_id.values())
|
|
2422
2705
|
row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id_int)
|
|
2423
2706
|
normalized_record, _normalized_ambiguous_fields = _build_normalized_row_from_answers(
|
|
2424
2707
|
cast(list[JSONValue], answer_list),
|
|
2425
2708
|
selected_fields,
|
|
2426
2709
|
)
|
|
2427
|
-
if self._record_get_needs_schema_refresh(
|
|
2710
|
+
if schema_available and self._record_get_needs_schema_refresh(
|
|
2428
2711
|
answer_list=cast(list[JSONValue], answer_list),
|
|
2429
2712
|
selected_fields=selected_fields,
|
|
2430
2713
|
record=row,
|
|
@@ -2440,7 +2723,6 @@ class RecordTools(ToolBase):
|
|
|
2440
2723
|
index = _build_top_level_field_index(schema)
|
|
2441
2724
|
selected_fields = list(index.by_id.values())
|
|
2442
2725
|
|
|
2443
|
-
unavailable_context: list[JSONObject] = []
|
|
2444
2726
|
dynamic_reference_answers, dynamic_reference_unavailable = self._record_get_dynamic_reference_answers(
|
|
2445
2727
|
context,
|
|
2446
2728
|
app_key=app_key,
|
|
@@ -2599,7 +2881,20 @@ class RecordTools(ToolBase):
|
|
|
2599
2881
|
) -> JSONObject:
|
|
2600
2882
|
"""执行内部辅助逻辑。"""
|
|
2601
2883
|
if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
2602
|
-
return self.
|
|
2884
|
+
return self._get_custom_view_browse_schema(
|
|
2885
|
+
profile,
|
|
2886
|
+
context,
|
|
2887
|
+
resolved_view.view_selection.view_key,
|
|
2888
|
+
force_refresh=force_refresh,
|
|
2889
|
+
)
|
|
2890
|
+
if resolved_view.kind == "system" and resolved_view.list_type is not None:
|
|
2891
|
+
return self._get_system_browse_schema(
|
|
2892
|
+
profile,
|
|
2893
|
+
context,
|
|
2894
|
+
app_key,
|
|
2895
|
+
list_type=resolved_view.list_type,
|
|
2896
|
+
force_refresh=force_refresh,
|
|
2897
|
+
)
|
|
2603
2898
|
return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
|
|
2604
2899
|
|
|
2605
2900
|
def _record_get_audit_info(
|
|
@@ -2660,7 +2955,7 @@ class RecordTools(ToolBase):
|
|
|
2660
2955
|
)
|
|
2661
2956
|
return result if isinstance(result, dict) else {"value": result}, list_type, role
|
|
2662
2957
|
except QingflowApiError as exc:
|
|
2663
|
-
if resolved_view.list_type is not None or exc
|
|
2958
|
+
if resolved_view.list_type is not None or not _is_record_permission_denied_error(exc):
|
|
2664
2959
|
raise
|
|
2665
2960
|
last_error: QingflowApiError = exc
|
|
2666
2961
|
for fallback_list_type in (14, 1, 2, 12):
|
|
@@ -2678,7 +2973,7 @@ class RecordTools(ToolBase):
|
|
|
2678
2973
|
return result if isinstance(result, dict) else {"value": result}, fallback_list_type, role
|
|
2679
2974
|
except QingflowApiError as fallback_exc:
|
|
2680
2975
|
last_error = fallback_exc
|
|
2681
|
-
if fallback_exc
|
|
2976
|
+
if _is_record_permission_denied_error(fallback_exc):
|
|
2682
2977
|
continue
|
|
2683
2978
|
raise
|
|
2684
2979
|
raise last_error
|
|
@@ -2776,6 +3071,8 @@ class RecordTools(ToolBase):
|
|
|
2776
3071
|
if target_app_key == app_key and str(target_record_id) == str(source_record_id):
|
|
2777
3072
|
reference_payload["self_reference"] = True
|
|
2778
3073
|
except QingflowApiError as exc:
|
|
3074
|
+
if is_auth_like_error(exc):
|
|
3075
|
+
raise
|
|
2779
3076
|
unavailable = _record_detail_unavailable_context(
|
|
2780
3077
|
"reference_detail",
|
|
2781
3078
|
f"引用字段「{field.que_title}」的目标记录详情获取失败。",
|
|
@@ -2873,6 +3170,8 @@ class RecordTools(ToolBase):
|
|
|
2873
3170
|
json_body=body,
|
|
2874
3171
|
)
|
|
2875
3172
|
except QingflowApiError as exc:
|
|
3173
|
+
if is_auth_like_error(exc):
|
|
3174
|
+
raise
|
|
2876
3175
|
unavailable = _record_detail_unavailable_context(
|
|
2877
3176
|
"reference_runtime_match",
|
|
2878
3177
|
"动态引用字段匹配数据获取失败。",
|
|
@@ -2927,6 +3226,8 @@ class RecordTools(ToolBase):
|
|
|
2927
3226
|
},
|
|
2928
3227
|
)
|
|
2929
3228
|
except QingflowApiError as exc:
|
|
3229
|
+
if is_auth_like_error(exc):
|
|
3230
|
+
raise
|
|
2930
3231
|
unavailable_context.append(_record_detail_unavailable_context("log_visibility", "侧边栏日志可见性获取失败。", exc))
|
|
2931
3232
|
return {"status": "unavailable", "channel": channel, "data_log_visible": None, "workflow_log_visible": None}
|
|
2932
3233
|
payload = visibility if isinstance(visibility, dict) else {}
|
|
@@ -2980,6 +3281,8 @@ class RecordTools(ToolBase):
|
|
|
2980
3281
|
source="data_logs",
|
|
2981
3282
|
)
|
|
2982
3283
|
except QingflowApiError as exc:
|
|
3284
|
+
if is_auth_like_error(exc):
|
|
3285
|
+
raise
|
|
2983
3286
|
unavailable_context.append(_record_detail_unavailable_context("data_logs", "最近数据日志获取失败。", exc))
|
|
2984
3287
|
return _record_detail_log_unavailable_payload("data_logs", "fetch_unavailable")
|
|
2985
3288
|
|
|
@@ -3033,6 +3336,8 @@ class RecordTools(ToolBase):
|
|
|
3033
3336
|
source="workflow_logs",
|
|
3034
3337
|
)
|
|
3035
3338
|
except QingflowApiError as exc:
|
|
3339
|
+
if is_auth_like_error(exc):
|
|
3340
|
+
raise
|
|
3036
3341
|
unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "流程日志本次获取失败。", exc))
|
|
3037
3342
|
return _record_detail_log_unavailable_payload("workflow_logs", "fetch_unavailable")
|
|
3038
3343
|
|
|
@@ -3076,6 +3381,8 @@ class RecordTools(ToolBase):
|
|
|
3076
3381
|
deadline=deadline,
|
|
3077
3382
|
)
|
|
3078
3383
|
except QingflowApiError as exc:
|
|
3384
|
+
if is_auth_like_error(exc):
|
|
3385
|
+
raise
|
|
3079
3386
|
unavailable_context.append(_record_detail_unavailable_context("data_logs", "全量数据日志获取失败。", exc))
|
|
3080
3387
|
return _record_logs_unavailable_payload("data_logs", "fetch_unavailable")
|
|
3081
3388
|
|
|
@@ -3135,6 +3442,8 @@ class RecordTools(ToolBase):
|
|
|
3135
3442
|
deadline=deadline,
|
|
3136
3443
|
)
|
|
3137
3444
|
except QingflowApiError as exc:
|
|
3445
|
+
if is_auth_like_error(exc):
|
|
3446
|
+
raise
|
|
3138
3447
|
unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "全量流程日志获取失败。", exc))
|
|
3139
3448
|
return _record_logs_unavailable_payload("workflow_logs", "fetch_unavailable")
|
|
3140
3449
|
|
|
@@ -3167,6 +3476,8 @@ class RecordTools(ToolBase):
|
|
|
3167
3476
|
params["auditNodeId"] = audit_node_id
|
|
3168
3477
|
payload = self.backend.request("GET", context, f"/app/{app_key}/asosChart", params=params)
|
|
3169
3478
|
except QingflowApiError as exc:
|
|
3479
|
+
if is_auth_like_error(exc):
|
|
3480
|
+
raise
|
|
3170
3481
|
unavailable_context.append(_record_detail_unavailable_context("associated_resources", "关联资源获取失败。", exc))
|
|
3171
3482
|
return []
|
|
3172
3483
|
return [_record_detail_associated_resource(item) for item in _record_detail_associated_resource_items(payload)]
|
|
@@ -3204,16 +3515,17 @@ class RecordTools(ToolBase):
|
|
|
3204
3515
|
refresh_source_url=refresh_source_url,
|
|
3205
3516
|
)
|
|
3206
3517
|
except Exception as exc: # defensive: media should never break the core record detail.
|
|
3518
|
+
warning: JSONObject = {
|
|
3519
|
+
"code": "MEDIA_ASSETS_UNAVAILABLE",
|
|
3520
|
+
"message": f"record_get could not collect media assets: {exc}",
|
|
3521
|
+
}
|
|
3522
|
+
if isinstance(exc, QingflowApiError):
|
|
3523
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
3207
3524
|
return {
|
|
3208
3525
|
"status": "unavailable",
|
|
3209
3526
|
"local_dir": None,
|
|
3210
3527
|
"items": [],
|
|
3211
|
-
"warnings": [
|
|
3212
|
-
{
|
|
3213
|
-
"code": "MEDIA_ASSETS_UNAVAILABLE",
|
|
3214
|
-
"message": f"record_get could not collect media assets: {exc}",
|
|
3215
|
-
}
|
|
3216
|
-
],
|
|
3528
|
+
"warnings": [warning],
|
|
3217
3529
|
}
|
|
3218
3530
|
|
|
3219
3531
|
def _record_get_file_assets(
|
|
@@ -3251,16 +3563,17 @@ class RecordTools(ToolBase):
|
|
|
3251
3563
|
refresh_source_url=refresh_source_url,
|
|
3252
3564
|
)
|
|
3253
3565
|
except Exception as exc: # defensive: file assets should never break the core record detail.
|
|
3566
|
+
warning = {
|
|
3567
|
+
"code": "FILE_ASSETS_UNAVAILABLE",
|
|
3568
|
+
"message": f"record_get could not collect file assets: {exc}",
|
|
3569
|
+
}
|
|
3570
|
+
if isinstance(exc, QingflowApiError):
|
|
3571
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
3254
3572
|
return {
|
|
3255
3573
|
"status": "unavailable",
|
|
3256
3574
|
"local_dir": None,
|
|
3257
3575
|
"items": [],
|
|
3258
|
-
"warnings": [
|
|
3259
|
-
{
|
|
3260
|
-
"code": "FILE_ASSETS_UNAVAILABLE",
|
|
3261
|
-
"message": f"record_get could not collect file assets: {exc}",
|
|
3262
|
-
}
|
|
3263
|
-
],
|
|
3576
|
+
"warnings": [warning],
|
|
3264
3577
|
}
|
|
3265
3578
|
|
|
3266
3579
|
def _record_get_refreshed_media_source_url(
|
|
@@ -3272,7 +3585,7 @@ class RecordTools(ToolBase):
|
|
|
3272
3585
|
resolved_view: AccessibleViewRoute,
|
|
3273
3586
|
audit_node_id: int | None,
|
|
3274
3587
|
candidate: JSONObject,
|
|
3275
|
-
) ->
|
|
3588
|
+
) -> JSONValue | None:
|
|
3276
3589
|
"""Refresh the detail payload once to recover an expired attachment storage signature."""
|
|
3277
3590
|
if candidate.get("source") not in {"attachment", "image_field", "subtable"}:
|
|
3278
3591
|
return None
|
|
@@ -3288,8 +3601,15 @@ class RecordTools(ToolBase):
|
|
|
3288
3601
|
resolved_view=resolved_view,
|
|
3289
3602
|
audit_node_id=audit_node_id,
|
|
3290
3603
|
)
|
|
3291
|
-
except QingflowApiError:
|
|
3292
|
-
return
|
|
3604
|
+
except QingflowApiError as exc:
|
|
3605
|
+
return {
|
|
3606
|
+
"source_url": None,
|
|
3607
|
+
"warning": _record_detail_unavailable_context(
|
|
3608
|
+
"asset_url_refresh",
|
|
3609
|
+
"record_get could not refresh the record detail before downloading a private asset.",
|
|
3610
|
+
exc,
|
|
3611
|
+
),
|
|
3612
|
+
}
|
|
3293
3613
|
for answer in _record_detail_answers(detail_result):
|
|
3294
3614
|
if not isinstance(answer, dict) or _coerce_count(answer.get("queId")) != field_id:
|
|
3295
3615
|
continue
|
|
@@ -3804,6 +4124,7 @@ class RecordTools(ToolBase):
|
|
|
3804
4124
|
record_id: Any | None,
|
|
3805
4125
|
fields: JSONObject | None = None,
|
|
3806
4126
|
items: list[JSONObject] | None = None,
|
|
4127
|
+
view_id: str | None = None,
|
|
3807
4128
|
dry_run: bool = False,
|
|
3808
4129
|
verify_write: bool = True,
|
|
3809
4130
|
output_profile: str = "normal",
|
|
@@ -3824,6 +4145,7 @@ class RecordTools(ToolBase):
|
|
|
3824
4145
|
profile=profile,
|
|
3825
4146
|
app_key=app_key,
|
|
3826
4147
|
items=normalized_items,
|
|
4148
|
+
view_id=view_id,
|
|
3827
4149
|
dry_run=dry_run,
|
|
3828
4150
|
verify_write=verify_write,
|
|
3829
4151
|
output_profile=normalized_output_profile,
|
|
@@ -3840,6 +4162,7 @@ class RecordTools(ToolBase):
|
|
|
3840
4162
|
app_key=app_key,
|
|
3841
4163
|
record_id=record_id_int,
|
|
3842
4164
|
fields=cast(JSONObject, fields or {}),
|
|
4165
|
+
view_id=view_id,
|
|
3843
4166
|
verify_write=verify_write,
|
|
3844
4167
|
output_profile=normalized_output_profile,
|
|
3845
4168
|
)
|
|
@@ -3851,17 +4174,61 @@ class RecordTools(ToolBase):
|
|
|
3851
4174
|
app_key: str,
|
|
3852
4175
|
record_id: int,
|
|
3853
4176
|
fields: JSONObject,
|
|
4177
|
+
view_id: str | None,
|
|
3854
4178
|
verify_write: bool,
|
|
3855
4179
|
output_profile: str,
|
|
4180
|
+
capture_exceptions: bool = False,
|
|
3856
4181
|
) -> JSONObject:
|
|
3857
4182
|
"""执行内部辅助逻辑。"""
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
4183
|
+
write_state = {"attempted": False}
|
|
4184
|
+
try:
|
|
4185
|
+
return self._record_update_public_single_impl(
|
|
4186
|
+
profile=profile,
|
|
4187
|
+
app_key=app_key,
|
|
4188
|
+
record_id=record_id,
|
|
4189
|
+
fields=fields,
|
|
4190
|
+
view_id=view_id,
|
|
4191
|
+
verify_write=verify_write,
|
|
4192
|
+
output_profile=output_profile,
|
|
4193
|
+
write_attempted_ref=lambda value: write_state.__setitem__("attempted", value),
|
|
4194
|
+
)
|
|
4195
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
4196
|
+
if not capture_exceptions:
|
|
4197
|
+
raise
|
|
4198
|
+
return self._record_write_exception_response(
|
|
4199
|
+
exc,
|
|
4200
|
+
operation="update",
|
|
4201
|
+
profile=profile,
|
|
4202
|
+
app_key=app_key,
|
|
4203
|
+
record_id=record_id,
|
|
4204
|
+
output_profile=output_profile,
|
|
4205
|
+
human_review=True,
|
|
4206
|
+
write_executed=write_state["attempted"],
|
|
4207
|
+
)
|
|
4208
|
+
|
|
4209
|
+
def _record_update_public_single_impl(
|
|
4210
|
+
self,
|
|
4211
|
+
*,
|
|
4212
|
+
profile: str,
|
|
4213
|
+
app_key: str,
|
|
4214
|
+
record_id: int,
|
|
4215
|
+
fields: JSONObject,
|
|
4216
|
+
view_id: str | None,
|
|
4217
|
+
verify_write: bool,
|
|
4218
|
+
output_profile: str,
|
|
4219
|
+
write_attempted_ref: Callable[[bool], None],
|
|
4220
|
+
) -> JSONObject:
|
|
4221
|
+
"""执行内部辅助逻辑。"""
|
|
4222
|
+
preflight_kwargs: dict[str, Any] = {
|
|
4223
|
+
"profile": profile,
|
|
4224
|
+
"app_key": app_key,
|
|
4225
|
+
"record_id": record_id,
|
|
4226
|
+
"fields": fields,
|
|
4227
|
+
"force_refresh_form": False,
|
|
4228
|
+
}
|
|
4229
|
+
if view_id is not None:
|
|
4230
|
+
preflight_kwargs["preferred_view_id"] = view_id
|
|
4231
|
+
raw_preflight = self._preflight_record_update_with_auto_view(**preflight_kwargs)
|
|
3865
4232
|
preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
|
|
3866
4233
|
preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
3867
4234
|
normalized_payload = self._record_write_normalized_payload(
|
|
@@ -3881,6 +4248,7 @@ class RecordTools(ToolBase):
|
|
|
3881
4248
|
human_review=True,
|
|
3882
4249
|
target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
|
|
3883
4250
|
)
|
|
4251
|
+
write_attempted_ref(True)
|
|
3884
4252
|
route_apply, tried_routes, route_blocker = self._record_update_apply_with_auto_route(
|
|
3885
4253
|
profile=profile,
|
|
3886
4254
|
app_key=app_key,
|
|
@@ -4161,7 +4529,9 @@ class RecordTools(ToolBase):
|
|
|
4161
4529
|
)
|
|
4162
4530
|
|
|
4163
4531
|
def _record_update_route_permission_denied(self, exc: QingflowApiError) -> bool:
|
|
4164
|
-
if exc
|
|
4532
|
+
if is_auth_like_error(exc):
|
|
4533
|
+
return False
|
|
4534
|
+
if backend_code_int(exc) in {40002, 40027, 40038, 404}:
|
|
4165
4535
|
return True
|
|
4166
4536
|
if exc.http_status == 404:
|
|
4167
4537
|
return True
|
|
@@ -4265,6 +4635,8 @@ class RecordTools(ToolBase):
|
|
|
4265
4635
|
},
|
|
4266
4636
|
)
|
|
4267
4637
|
except QingflowApiError as exc:
|
|
4638
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
4639
|
+
raise
|
|
4268
4640
|
return unavailable(
|
|
4269
4641
|
error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
|
|
4270
4642
|
reason="current-user todo task list is unavailable",
|
|
@@ -4312,6 +4684,8 @@ class RecordTools(ToolBase):
|
|
|
4312
4684
|
f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
|
|
4313
4685
|
)
|
|
4314
4686
|
except QingflowApiError as exc:
|
|
4687
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
4688
|
+
raise
|
|
4315
4689
|
return unavailable(
|
|
4316
4690
|
error_code="TASK_EDITABLE_FIELDS_UNAVAILABLE",
|
|
4317
4691
|
reason="workflow node editable field list is unavailable; record_update will not guess task editability",
|
|
@@ -4462,7 +4836,7 @@ class RecordTools(ToolBase):
|
|
|
4462
4836
|
raise_tool_error(QingflowApiError.config_error("view_key is required for custom view update"))
|
|
4463
4837
|
|
|
4464
4838
|
def runner(session_profile, context):
|
|
4465
|
-
index = self.
|
|
4839
|
+
index = self._get_view_field_index(profile, context, normalized_view_key, force_refresh=force_refresh_form) if verify_write else None
|
|
4466
4840
|
normalized_answers = [dict(item) for item in answers if isinstance(item, dict)]
|
|
4467
4841
|
self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
|
|
4468
4842
|
result = self.backend.request(
|
|
@@ -4565,6 +4939,7 @@ class RecordTools(ToolBase):
|
|
|
4565
4939
|
profile: str,
|
|
4566
4940
|
app_key: str,
|
|
4567
4941
|
items: list[JSONObject],
|
|
4942
|
+
view_id: str | None,
|
|
4568
4943
|
dry_run: bool,
|
|
4569
4944
|
verify_write: bool,
|
|
4570
4945
|
output_profile: str,
|
|
@@ -4576,6 +4951,7 @@ class RecordTools(ToolBase):
|
|
|
4576
4951
|
app_key=app_key,
|
|
4577
4952
|
record_id=cast(int, item["record_id"]),
|
|
4578
4953
|
fields=cast(JSONObject, item["fields"]),
|
|
4954
|
+
view_id=view_id,
|
|
4579
4955
|
output_profile=output_profile,
|
|
4580
4956
|
)
|
|
4581
4957
|
for item in items
|
|
@@ -4604,8 +4980,10 @@ class RecordTools(ToolBase):
|
|
|
4604
4980
|
app_key=app_key,
|
|
4605
4981
|
record_id=record_id,
|
|
4606
4982
|
fields=fields,
|
|
4983
|
+
view_id=view_id,
|
|
4607
4984
|
verify_write=verify_write,
|
|
4608
4985
|
output_profile=output_profile,
|
|
4986
|
+
capture_exceptions=True,
|
|
4609
4987
|
)
|
|
4610
4988
|
)
|
|
4611
4989
|
except (QingflowApiError, RuntimeError) as exc:
|
|
@@ -4636,16 +5014,20 @@ class RecordTools(ToolBase):
|
|
|
4636
5014
|
app_key: str,
|
|
4637
5015
|
record_id: int,
|
|
4638
5016
|
fields: JSONObject,
|
|
5017
|
+
view_id: str | None,
|
|
4639
5018
|
output_profile: str,
|
|
4640
5019
|
) -> JSONObject:
|
|
4641
5020
|
"""执行内部辅助逻辑。"""
|
|
4642
|
-
|
|
4643
|
-
profile
|
|
4644
|
-
app_key
|
|
4645
|
-
record_id
|
|
4646
|
-
fields
|
|
4647
|
-
force_refresh_form
|
|
4648
|
-
|
|
5021
|
+
preflight_kwargs: dict[str, Any] = {
|
|
5022
|
+
"profile": profile,
|
|
5023
|
+
"app_key": app_key,
|
|
5024
|
+
"record_id": record_id,
|
|
5025
|
+
"fields": fields,
|
|
5026
|
+
"force_refresh_form": False,
|
|
5027
|
+
}
|
|
5028
|
+
if view_id is not None:
|
|
5029
|
+
preflight_kwargs["preferred_view_id"] = view_id
|
|
5030
|
+
raw_preflight = self._preflight_record_update_with_auto_view(**preflight_kwargs)
|
|
4649
5031
|
preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
4650
5032
|
normalized_payload = self._record_write_normalized_payload(
|
|
4651
5033
|
operation="update",
|
|
@@ -4851,6 +5233,9 @@ class RecordTools(ToolBase):
|
|
|
4851
5233
|
item: JSONObject = {
|
|
4852
5234
|
"resource": data.get("resource"),
|
|
4853
5235
|
"status": response.get("status"),
|
|
5236
|
+
"write_executed": bool(response.get("write_executed")),
|
|
5237
|
+
"safe_to_retry": bool(response.get("safe_to_retry", True)),
|
|
5238
|
+
"verification_status": response.get("verification_status", "not_requested"),
|
|
4854
5239
|
"verification": data.get("verification"),
|
|
4855
5240
|
"field_errors": cast(list[JSONObject], data.get("field_errors", [])),
|
|
4856
5241
|
"confirmation_requests": cast(list[JSONObject], data.get("confirmation_requests", [])),
|
|
@@ -4860,7 +5245,7 @@ class RecordTools(ToolBase):
|
|
|
4860
5245
|
if isinstance(update_route, dict):
|
|
4861
5246
|
item["update_route"] = update_route
|
|
4862
5247
|
tried_routes = response.get("tried_routes")
|
|
4863
|
-
if isinstance(tried_routes, list):
|
|
5248
|
+
if isinstance(tried_routes, list) and (output_profile == "verbose" or response.get("status") != "success"):
|
|
4864
5249
|
item["tried_routes"] = tried_routes
|
|
4865
5250
|
blockers = data.get("blockers")
|
|
4866
5251
|
if isinstance(blockers, list) and blockers:
|
|
@@ -4882,6 +5267,7 @@ class RecordTools(ToolBase):
|
|
|
4882
5267
|
app_key: str,
|
|
4883
5268
|
record_id: int,
|
|
4884
5269
|
fields: JSONObject,
|
|
5270
|
+
preferred_view_id: str | None = None,
|
|
4885
5271
|
force_refresh_form: bool,
|
|
4886
5272
|
) -> JSONObject:
|
|
4887
5273
|
"""执行内部辅助逻辑。"""
|
|
@@ -4889,6 +5275,25 @@ class RecordTools(ToolBase):
|
|
|
4889
5275
|
request_route = self._request_route_payload(context)
|
|
4890
5276
|
def build_once(*, effective_force_refresh: bool) -> JSONObject:
|
|
4891
5277
|
candidate_routes = self._candidate_update_views(profile, context, app_key)
|
|
5278
|
+
normalized_preferred_view_id = _normalize_optional_text(preferred_view_id)
|
|
5279
|
+
if normalized_preferred_view_id:
|
|
5280
|
+
preferred_route = next(
|
|
5281
|
+
(
|
|
5282
|
+
route
|
|
5283
|
+
for route in candidate_routes
|
|
5284
|
+
if route.view_id == normalized_preferred_view_id
|
|
5285
|
+
),
|
|
5286
|
+
None,
|
|
5287
|
+
)
|
|
5288
|
+
if preferred_route is None:
|
|
5289
|
+
raise_tool_error(
|
|
5290
|
+
QingflowApiError.config_error(
|
|
5291
|
+
f"view_id '{normalized_preferred_view_id}' is not an accessible update candidate"
|
|
5292
|
+
)
|
|
5293
|
+
)
|
|
5294
|
+
candidate_routes = [preferred_route]
|
|
5295
|
+
else:
|
|
5296
|
+
candidate_routes = _prefer_custom_update_routes(candidate_routes)
|
|
4892
5297
|
probes = self._probe_candidate_record_contexts(
|
|
4893
5298
|
context,
|
|
4894
5299
|
app_key=app_key,
|
|
@@ -5102,41 +5507,24 @@ class RecordTools(ToolBase):
|
|
|
5102
5507
|
"data": first_confirmation_plan,
|
|
5103
5508
|
}
|
|
5104
5509
|
|
|
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
|
|
5510
|
+
if normalized_preferred_view_id and first_blocked_plan is not None:
|
|
5511
|
+
first_blocked_plan["view_probe_summary"] = probe_summary
|
|
5512
|
+
first_blocked_plan["record_context_probe"] = probe_summary
|
|
5127
5513
|
return {
|
|
5128
5514
|
"profile": profile,
|
|
5129
5515
|
"ws_id": session_profile.selected_ws_id,
|
|
5130
5516
|
"ok": True,
|
|
5131
5517
|
"request_route": request_route,
|
|
5132
|
-
"data":
|
|
5518
|
+
"data": first_blocked_plan,
|
|
5133
5519
|
}
|
|
5134
5520
|
|
|
5135
5521
|
blocked_data = self._build_auto_view_blocked_preflight_data(
|
|
5136
5522
|
app_key=app_key,
|
|
5137
5523
|
record_id=record_id,
|
|
5138
5524
|
blockers=["NO_SINGLE_VIEW_CAN_UPDATE_ALL_FIELDS"],
|
|
5139
|
-
warnings=[
|
|
5525
|
+
warnings=[
|
|
5526
|
+
"record_update requires one executable frontend route for the full payload; it does not merge writable fields across multiple views."
|
|
5527
|
+
],
|
|
5140
5528
|
recommended_next_actions=[
|
|
5141
5529
|
"Call record_update_schema_get first to inspect the overall writable field set for this record.",
|
|
5142
5530
|
"Reduce the update payload until all requested fields fit inside one matched accessible view.",
|
|
@@ -5183,6 +5571,7 @@ class RecordTools(ToolBase):
|
|
|
5183
5571
|
union_writable_field_ids: set[int] = set()
|
|
5184
5572
|
union_visible_question_ids: set[int] = set()
|
|
5185
5573
|
matched_view_payloads: list[JSONObject] = []
|
|
5574
|
+
union_index: FieldIndex | None = None
|
|
5186
5575
|
|
|
5187
5576
|
for candidate in matched_routes:
|
|
5188
5577
|
browse_scope = self._build_browse_write_scope(
|
|
@@ -5192,11 +5581,13 @@ class RecordTools(ToolBase):
|
|
|
5192
5581
|
candidate,
|
|
5193
5582
|
force_refresh=force_refresh_form,
|
|
5194
5583
|
)
|
|
5584
|
+
browse_index = cast(FieldIndex, browse_scope["index"])
|
|
5585
|
+
union_index = browse_index if union_index is None else _merge_field_indexes(union_index, browse_index)
|
|
5195
5586
|
union_writable_field_ids.update(cast(set[int], browse_scope["writable_field_ids"]))
|
|
5196
5587
|
union_visible_question_ids.update(cast(set[int], browse_scope["visible_question_ids"]))
|
|
5197
5588
|
matched_view_payloads.append(_accessible_view_payload(candidate))
|
|
5198
5589
|
|
|
5199
|
-
if not union_writable_field_ids and not union_visible_question_ids:
|
|
5590
|
+
if union_index is None or (not union_writable_field_ids and not union_visible_question_ids):
|
|
5200
5591
|
return None
|
|
5201
5592
|
|
|
5202
5593
|
plan_data = self._build_record_write_preflight(
|
|
@@ -5213,10 +5604,9 @@ class RecordTools(ToolBase):
|
|
|
5213
5604
|
view_key=None,
|
|
5214
5605
|
view_name=None,
|
|
5215
5606
|
existing_answers_override=current_answers,
|
|
5607
|
+
field_index_override=union_index,
|
|
5216
5608
|
)
|
|
5217
5609
|
|
|
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
5610
|
validation = cast(JSONObject, plan_data.get("validation", {}))
|
|
5221
5611
|
invalid_fields = cast(list[JSONObject], validation.get("invalid_fields", []))
|
|
5222
5612
|
missing_required_fields = cast(list[JSONObject], validation.get("missing_required_fields", []))
|
|
@@ -5227,12 +5617,21 @@ class RecordTools(ToolBase):
|
|
|
5227
5617
|
invalid_fields.extend(
|
|
5228
5618
|
self._validate_view_scoped_subtable_answers(
|
|
5229
5619
|
normalized_answers=cast(list[JSONObject], plan_data.get("normalized_answers", [])),
|
|
5230
|
-
full_index=
|
|
5231
|
-
selector_index=
|
|
5620
|
+
full_index=union_index,
|
|
5621
|
+
selector_index=union_index,
|
|
5232
5622
|
visible_question_ids=union_visible_question_ids,
|
|
5233
5623
|
)
|
|
5234
5624
|
)
|
|
5235
5625
|
|
|
5626
|
+
readonly_or_system_fields = [
|
|
5627
|
+
item
|
|
5628
|
+
for item in readonly_or_system_fields
|
|
5629
|
+
if not (
|
|
5630
|
+
isinstance(item, dict)
|
|
5631
|
+
and (que_id := _coerce_count(item.get("que_id"))) is not None
|
|
5632
|
+
and que_id in union_writable_field_ids
|
|
5633
|
+
)
|
|
5634
|
+
]
|
|
5236
5635
|
existing_readonly_ids = {
|
|
5237
5636
|
str(_coerce_count(item.get("que_id")))
|
|
5238
5637
|
for item in readonly_or_system_fields
|
|
@@ -5396,7 +5795,13 @@ class RecordTools(ToolBase):
|
|
|
5396
5795
|
view_type=None,
|
|
5397
5796
|
)
|
|
5398
5797
|
)
|
|
5399
|
-
|
|
5798
|
+
try:
|
|
5799
|
+
view_items = self._get_view_list(profile, context, app_key)
|
|
5800
|
+
except QingflowApiError as exc:
|
|
5801
|
+
if not _is_record_permission_denied_error(exc):
|
|
5802
|
+
raise
|
|
5803
|
+
view_items = []
|
|
5804
|
+
for item in view_items:
|
|
5400
5805
|
if not isinstance(item, dict):
|
|
5401
5806
|
continue
|
|
5402
5807
|
view_key = _normalize_optional_text(item.get("viewKey"))
|
|
@@ -5453,7 +5858,9 @@ class RecordTools(ToolBase):
|
|
|
5453
5858
|
return payload
|
|
5454
5859
|
|
|
5455
5860
|
def _is_record_context_route_miss(self, error: QingflowApiError) -> bool:
|
|
5456
|
-
if error
|
|
5861
|
+
if is_auth_like_error(error):
|
|
5862
|
+
return False
|
|
5863
|
+
if backend_code_int(error) in {40002, 40023, 40027, 40038, 404}:
|
|
5457
5864
|
return True
|
|
5458
5865
|
if error.http_status == 404:
|
|
5459
5866
|
return True
|
|
@@ -5483,11 +5890,12 @@ class RecordTools(ToolBase):
|
|
|
5483
5890
|
used_list_type = None
|
|
5484
5891
|
else:
|
|
5485
5892
|
used_list_type = resolved_view.list_type if resolved_view.list_type is not None else DEFAULT_RECORD_LIST_TYPE
|
|
5893
|
+
role = _record_detail_role_for_list_type(used_list_type)
|
|
5486
5894
|
record = self.backend.request(
|
|
5487
5895
|
"GET",
|
|
5488
5896
|
context,
|
|
5489
5897
|
f"/app/{app_key}/apply/{apply_id}",
|
|
5490
|
-
params={"role":
|
|
5898
|
+
params={"role": role, "listType": used_list_type},
|
|
5491
5899
|
)
|
|
5492
5900
|
answers = record.get("answers") if isinstance(record, dict) else None
|
|
5493
5901
|
normalized_answers = [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []
|
|
@@ -5517,6 +5925,8 @@ class RecordTools(ToolBase):
|
|
|
5517
5925
|
error_payload=None,
|
|
5518
5926
|
)
|
|
5519
5927
|
except QingflowApiError as exc:
|
|
5928
|
+
if not self._is_record_context_route_miss(exc):
|
|
5929
|
+
raise
|
|
5520
5930
|
return RecordContextRouteProbe(
|
|
5521
5931
|
route=resolved_view,
|
|
5522
5932
|
answer_list=None,
|
|
@@ -5588,7 +5998,7 @@ class RecordTools(ToolBase):
|
|
|
5588
5998
|
]
|
|
5589
5999
|
|
|
5590
6000
|
def _looks_like_generic_record_update_backend_failure(self, exc: QingflowApiError) -> bool:
|
|
5591
|
-
if exc
|
|
6001
|
+
if backend_code_int(exc) == 500:
|
|
5592
6002
|
return True
|
|
5593
6003
|
if exc.http_status is not None and exc.http_status >= 500:
|
|
5594
6004
|
return True
|
|
@@ -5713,12 +6123,15 @@ class RecordTools(ToolBase):
|
|
|
5713
6123
|
app_key: str,
|
|
5714
6124
|
record_id: Any | None = None,
|
|
5715
6125
|
record_ids: list[Any] | None = None,
|
|
6126
|
+
view_id: str | None = None,
|
|
6127
|
+
list_type: int | None = None,
|
|
5716
6128
|
output_profile: str = "normal",
|
|
5717
6129
|
) -> JSONObject:
|
|
5718
6130
|
"""执行记录相关逻辑。"""
|
|
5719
6131
|
normalized_output_profile = self._normalize_public_output_profile(output_profile)
|
|
5720
6132
|
if not app_key:
|
|
5721
6133
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
6134
|
+
delete_list_type = self._resolve_record_delete_list_type(view_id=view_id, list_type=list_type)
|
|
5722
6135
|
normalized_record_ids: list[int] = []
|
|
5723
6136
|
for index, item in enumerate(record_ids or []):
|
|
5724
6137
|
normalized_record_ids.append(normalize_positive_id_int(item, field_name=f"record_ids[{index}]"))
|
|
@@ -5738,21 +6151,72 @@ class RecordTools(ToolBase):
|
|
|
5738
6151
|
"record_ids": [stringify_backend_id(item) for item in delete_ids],
|
|
5739
6152
|
"answers": [],
|
|
5740
6153
|
"submit_type": 1,
|
|
6154
|
+
"selection": {"view_id": view_id, "list_type": delete_list_type},
|
|
5741
6155
|
}
|
|
5742
6156
|
return self._record_delete_public_batch(
|
|
5743
6157
|
profile=profile,
|
|
5744
6158
|
app_key=app_key,
|
|
5745
6159
|
delete_ids=delete_ids,
|
|
6160
|
+
list_type=delete_list_type,
|
|
5746
6161
|
normalized_payload=normalized_payload,
|
|
5747
6162
|
output_profile=normalized_output_profile,
|
|
5748
6163
|
)
|
|
5749
6164
|
|
|
6165
|
+
def _resolve_record_delete_list_type(self, *, view_id: str | None, list_type: int | None) -> int:
|
|
6166
|
+
normalized_view_id = _normalize_optional_text(view_id)
|
|
6167
|
+
if normalized_view_id:
|
|
6168
|
+
if normalized_view_id.startswith("custom:"):
|
|
6169
|
+
raise_tool_error(
|
|
6170
|
+
QingflowApiError.config_error(
|
|
6171
|
+
"record_delete does not support custom view deletion; the backend delete route accepts system listType only",
|
|
6172
|
+
details={
|
|
6173
|
+
"error_code": "RECORD_DELETE_CUSTOM_VIEW_UNSUPPORTED",
|
|
6174
|
+
"view_id": normalized_view_id,
|
|
6175
|
+
"fix_hint": (
|
|
6176
|
+
"Use a system view_id from app_get.accessible_views, or resolve target record_ids with "
|
|
6177
|
+
"record list/get first and retry delete without a custom view selector."
|
|
6178
|
+
),
|
|
6179
|
+
},
|
|
6180
|
+
)
|
|
6181
|
+
)
|
|
6182
|
+
if not normalized_view_id.startswith("system:"):
|
|
6183
|
+
raise_tool_error(QingflowApiError.config_error("view_id must start with system: or custom:"))
|
|
6184
|
+
mapped_list_type = SYSTEM_VIEW_ID_TO_LIST_TYPE.get(normalized_view_id)
|
|
6185
|
+
if mapped_list_type is None:
|
|
6186
|
+
raise_tool_error(QingflowApiError.config_error(f"unsupported view_id '{normalized_view_id}'"))
|
|
6187
|
+
return mapped_list_type
|
|
6188
|
+
if list_type is not None:
|
|
6189
|
+
normalized_list_type = int(list_type)
|
|
6190
|
+
if normalized_list_type not in SYSTEM_VIEW_LIST_TYPES:
|
|
6191
|
+
raise_tool_error(
|
|
6192
|
+
QingflowApiError.config_error(
|
|
6193
|
+
"record_delete list_type must map to a supported system view",
|
|
6194
|
+
details={
|
|
6195
|
+
"error_code": "RECORD_DELETE_SYSTEM_VIEW_REQUIRED",
|
|
6196
|
+
"list_type": normalized_list_type,
|
|
6197
|
+
"supported_list_types": sorted(SYSTEM_VIEW_LIST_TYPES),
|
|
6198
|
+
"fix_hint": "Pass a system view_id from app_get.accessible_views instead of an arbitrary list_type.",
|
|
6199
|
+
},
|
|
6200
|
+
)
|
|
6201
|
+
)
|
|
6202
|
+
return normalized_list_type
|
|
6203
|
+
raise_tool_error(
|
|
6204
|
+
QingflowApiError.config_error(
|
|
6205
|
+
"record_delete requires a system view_id or list_type; deleting without frontend list context is ambiguous",
|
|
6206
|
+
details={
|
|
6207
|
+
"error_code": "RECORD_DELETE_VIEW_REQUIRED",
|
|
6208
|
+
"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.",
|
|
6209
|
+
},
|
|
6210
|
+
)
|
|
6211
|
+
)
|
|
6212
|
+
|
|
5750
6213
|
def _record_delete_public_batch(
|
|
5751
6214
|
self,
|
|
5752
6215
|
*,
|
|
5753
6216
|
profile: str,
|
|
5754
6217
|
app_key: str,
|
|
5755
6218
|
delete_ids: list[int],
|
|
6219
|
+
list_type: int,
|
|
5756
6220
|
normalized_payload: JSONObject,
|
|
5757
6221
|
output_profile: str,
|
|
5758
6222
|
) -> JSONObject:
|
|
@@ -5762,7 +6226,7 @@ class RecordTools(ToolBase):
|
|
|
5762
6226
|
for index, delete_id in enumerate(delete_ids):
|
|
5763
6227
|
record_id_text = stringify_backend_id(delete_id)
|
|
5764
6228
|
try:
|
|
5765
|
-
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=[delete_id])
|
|
6229
|
+
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=[delete_id], list_type=list_type)
|
|
5766
6230
|
request_route = cast(JSONObject, raw_apply.get("request_route")) if isinstance(raw_apply.get("request_route"), dict) else request_route
|
|
5767
6231
|
ws_id = raw_apply.get("ws_id", ws_id)
|
|
5768
6232
|
single_payload = {
|
|
@@ -5771,6 +6235,7 @@ class RecordTools(ToolBase):
|
|
|
5771
6235
|
"record_ids": [record_id_text],
|
|
5772
6236
|
"answers": [],
|
|
5773
6237
|
"submit_type": 1,
|
|
6238
|
+
"selection": normalized_payload.get("selection"),
|
|
5774
6239
|
}
|
|
5775
6240
|
single_response = self._record_write_apply_response(
|
|
5776
6241
|
raw_apply,
|
|
@@ -6053,12 +6518,13 @@ class RecordTools(ToolBase):
|
|
|
6053
6518
|
preflight=raw_preflight,
|
|
6054
6519
|
)
|
|
6055
6520
|
|
|
6056
|
-
if
|
|
6521
|
+
if view_key is not None or view_name is not None:
|
|
6057
6522
|
raise_tool_error(
|
|
6058
6523
|
QingflowApiError.config_error(
|
|
6059
|
-
"delete does not
|
|
6524
|
+
"delete does not support custom view selectors; use a system view_id/list_type or resolve target record_ids first"
|
|
6060
6525
|
)
|
|
6061
6526
|
)
|
|
6527
|
+
delete_list_type = self._resolve_record_delete_list_type(view_id=view_id, list_type=list_type)
|
|
6062
6528
|
if normalized_values or normalized_set:
|
|
6063
6529
|
raise_tool_error(QingflowApiError.config_error("delete must not include values or set"))
|
|
6064
6530
|
delete_ids = normalized_record_ids or ([record_id] if record_id is not None and record_id > 0 else [])
|
|
@@ -6070,8 +6536,9 @@ class RecordTools(ToolBase):
|
|
|
6070
6536
|
"record_ids": delete_ids,
|
|
6071
6537
|
"answers": [],
|
|
6072
6538
|
"submit_type": submit_type_value,
|
|
6539
|
+
"selection": {"view_id": view_id, "list_type": delete_list_type},
|
|
6073
6540
|
}
|
|
6074
|
-
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=delete_ids)
|
|
6541
|
+
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=delete_ids, list_type=delete_list_type)
|
|
6075
6542
|
return self._record_write_apply_response(
|
|
6076
6543
|
raw_apply,
|
|
6077
6544
|
operation="delete",
|
|
@@ -6221,7 +6688,9 @@ class RecordTools(ToolBase):
|
|
|
6221
6688
|
or _normalize_optional_text(payload.get("appName"))
|
|
6222
6689
|
or _normalize_optional_text(payload.get("appTitle"))
|
|
6223
6690
|
)
|
|
6224
|
-
except QingflowApiError:
|
|
6691
|
+
except QingflowApiError as exc:
|
|
6692
|
+
if is_auth_like_error(exc):
|
|
6693
|
+
raise
|
|
6225
6694
|
name = None
|
|
6226
6695
|
self._app_name_cache[cache_key] = name
|
|
6227
6696
|
return name
|
|
@@ -6375,7 +6844,9 @@ class RecordTools(ToolBase):
|
|
|
6375
6844
|
try:
|
|
6376
6845
|
result = self.backend.request("GET", context, "/data/baseInfo", params={"queId": field.que_id})
|
|
6377
6846
|
payload = result if isinstance(result, dict) else None
|
|
6378
|
-
except QingflowApiError:
|
|
6847
|
+
except QingflowApiError as exc:
|
|
6848
|
+
if is_auth_like_error(exc):
|
|
6849
|
+
raise
|
|
6379
6850
|
payload = None
|
|
6380
6851
|
self._relation_base_info_cache[cache_key] = payload or {}
|
|
6381
6852
|
return payload
|
|
@@ -6648,6 +7119,26 @@ class RecordTools(ToolBase):
|
|
|
6648
7119
|
or bool(fields)
|
|
6649
7120
|
)
|
|
6650
7121
|
|
|
7122
|
+
def _member_candidate_static_preview_should_use_backend(self, field: FormField) -> bool:
|
|
7123
|
+
"""Return true when the frontend field-scope endpoint is safer than directory expansion."""
|
|
7124
|
+
scope = field.member_select_scope if isinstance(field.member_select_scope, dict) else {}
|
|
7125
|
+
if field.member_select_scope_type != 2:
|
|
7126
|
+
return False
|
|
7127
|
+
return bool(
|
|
7128
|
+
_scope_has_dynamic_or_external(scope)
|
|
7129
|
+
or list(scope.get("depart") or [])
|
|
7130
|
+
or list(scope.get("role") or [])
|
|
7131
|
+
)
|
|
7132
|
+
|
|
7133
|
+
def _department_candidate_static_preview_should_use_backend(self, field: FormField) -> bool:
|
|
7134
|
+
"""Return true when static preview would otherwise need ContactAuth-only directory APIs."""
|
|
7135
|
+
scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
|
|
7136
|
+
if field.dept_select_scope_type != 2:
|
|
7137
|
+
return False
|
|
7138
|
+
if _scope_has_dynamic_or_external(scope):
|
|
7139
|
+
return True
|
|
7140
|
+
return bool(_normalize_bool(scope.get("includeSubDeparts")) or not list(scope.get("depart") or []))
|
|
7141
|
+
|
|
6651
7142
|
def _build_candidate_lookup_state(
|
|
6652
7143
|
self,
|
|
6653
7144
|
profile: str,
|
|
@@ -6666,7 +7157,9 @@ class RecordTools(ToolBase):
|
|
|
6666
7157
|
if apply_id is not None:
|
|
6667
7158
|
try:
|
|
6668
7159
|
base_answers = self._load_record_answers_for_preflight(context, app_key=app_key, apply_id=apply_id)
|
|
6669
|
-
except QingflowApiError:
|
|
7160
|
+
except QingflowApiError as exc:
|
|
7161
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
7162
|
+
raise
|
|
6670
7163
|
context_complete = False
|
|
6671
7164
|
state = LookupResolutionState(
|
|
6672
7165
|
operation="update" if apply_id is not None else "insert",
|
|
@@ -7156,15 +7649,16 @@ class RecordTools(ToolBase):
|
|
|
7156
7649
|
)
|
|
7157
7650
|
if configured_candidate is not None:
|
|
7158
7651
|
self._merge_department_candidate(merged, configured_candidate)
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7652
|
+
if include_sub:
|
|
7653
|
+
for dept in self._list_departments_by_scope(context, dept_id=dept_id, include_sub_departments=True):
|
|
7654
|
+
normalized = _normalize_candidate_department(
|
|
7655
|
+
dept,
|
|
7656
|
+
source_kind="department",
|
|
7657
|
+
source_id=dept_id,
|
|
7658
|
+
source_value=dept_name,
|
|
7659
|
+
)
|
|
7660
|
+
if normalized is not None:
|
|
7661
|
+
self._merge_department_candidate(merged, normalized)
|
|
7168
7662
|
filtered = _filter_department_candidates(list(merged.values()), keyword)
|
|
7169
7663
|
filtered.sort(key=lambda item: (_normalize_optional_text(item.get("value")) or "", _coerce_count(item.get("id")) or 0))
|
|
7170
7664
|
return filtered
|
|
@@ -8305,22 +8799,10 @@ class RecordTools(ToolBase):
|
|
|
8305
8799
|
field_index_override: FieldIndex | None = None,
|
|
8306
8800
|
) -> JSONObject:
|
|
8307
8801
|
"""执行内部辅助逻辑。"""
|
|
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
8802
|
normalized_fields = fields or {}
|
|
8321
8803
|
normalized_answers_input = answers or []
|
|
8322
8804
|
resolved_view: AccessibleViewRoute | None = None
|
|
8323
|
-
selector_index =
|
|
8805
|
+
selector_index: FieldIndex | None = field_index_override
|
|
8324
8806
|
browse_writable_field_ids: set[int] = set()
|
|
8325
8807
|
visible_question_ids: set[int] = set()
|
|
8326
8808
|
if any(item is not None for item in (view_id, list_type, view_key, view_name)):
|
|
@@ -8346,6 +8828,31 @@ class RecordTools(ToolBase):
|
|
|
8346
8828
|
visible_question_ids = cast(set[int], browse_scope["visible_question_ids"])
|
|
8347
8829
|
else:
|
|
8348
8830
|
compatibility_warnings = []
|
|
8831
|
+
if field_index_override is not None:
|
|
8832
|
+
base_index = field_index_override
|
|
8833
|
+
question_relations: list[JSONObject] = []
|
|
8834
|
+
runtime_linked_field_ids: set[int] = set()
|
|
8835
|
+
index = base_index
|
|
8836
|
+
elif operation == "update" and resolved_view is not None:
|
|
8837
|
+
base_index = cast(FieldIndex, selector_index)
|
|
8838
|
+
question_relations = []
|
|
8839
|
+
runtime_linked_field_ids = set()
|
|
8840
|
+
index = base_index
|
|
8841
|
+
else:
|
|
8842
|
+
schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
|
|
8843
|
+
base_index = _build_applicant_top_level_field_index(schema)
|
|
8844
|
+
question_relations = _collect_question_relations(schema)
|
|
8845
|
+
runtime_linked_field_ids = _collect_linked_required_field_ids(question_relations)
|
|
8846
|
+
runtime_linked_field_ids.update(_collect_option_linked_field_ids(base_index))
|
|
8847
|
+
index = base_index
|
|
8848
|
+
if operation == "create":
|
|
8849
|
+
linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
|
|
8850
|
+
schema,
|
|
8851
|
+
linked_field_ids=runtime_linked_field_ids,
|
|
8852
|
+
)
|
|
8853
|
+
index = _merge_field_indexes(base_index, linked_hidden_index)
|
|
8854
|
+
if selector_index is None:
|
|
8855
|
+
selector_index = index
|
|
8349
8856
|
resolved_fields = self._collect_write_plan_field_refs(fields=normalized_fields, answers=normalized_answers_input, index=selector_index)
|
|
8350
8857
|
support_matrix = _summarize_write_support(resolved_fields)
|
|
8351
8858
|
invalid_fields: list[JSONObject] = []
|
|
@@ -8389,7 +8896,9 @@ class RecordTools(ToolBase):
|
|
|
8389
8896
|
apply_id=apply_id,
|
|
8390
8897
|
)
|
|
8391
8898
|
existing_answers_loaded = True
|
|
8392
|
-
except QingflowApiError:
|
|
8899
|
+
except QingflowApiError as exc:
|
|
8900
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
8901
|
+
raise
|
|
8393
8902
|
validation_warnings.append(
|
|
8394
8903
|
"update preflight could not load the current record; required-field completeness and dynamic lookup context were not fully revalidated."
|
|
8395
8904
|
)
|
|
@@ -8978,7 +9487,7 @@ class RecordTools(ToolBase):
|
|
|
8978
9487
|
break
|
|
8979
9488
|
except QingflowApiError as exc:
|
|
8980
9489
|
last_error = exc
|
|
8981
|
-
if exc
|
|
9490
|
+
if _is_record_permission_denied_error(exc):
|
|
8982
9491
|
continue
|
|
8983
9492
|
raise
|
|
8984
9493
|
if result is None:
|
|
@@ -9081,7 +9590,21 @@ class RecordTools(ToolBase):
|
|
|
9081
9590
|
normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
|
|
9082
9591
|
|
|
9083
9592
|
def runner(session_profile, context):
|
|
9084
|
-
|
|
9593
|
+
needs_index = verify_write or bool(fields) or _answers_need_resolution(answers or [])
|
|
9594
|
+
update_index = None
|
|
9595
|
+
if needs_index:
|
|
9596
|
+
update_index = (
|
|
9597
|
+
self._get_system_browse_field_index(
|
|
9598
|
+
profile,
|
|
9599
|
+
context,
|
|
9600
|
+
app_key,
|
|
9601
|
+
list_type=DEFAULT_RECORD_LIST_TYPE,
|
|
9602
|
+
force_refresh=force_refresh_form,
|
|
9603
|
+
)
|
|
9604
|
+
if role == 1
|
|
9605
|
+
else self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
|
|
9606
|
+
)
|
|
9607
|
+
index = update_index if verify_write else None
|
|
9085
9608
|
normalized_answers = self._resolve_answers(
|
|
9086
9609
|
profile,
|
|
9087
9610
|
context,
|
|
@@ -9089,6 +9612,7 @@ class RecordTools(ToolBase):
|
|
|
9089
9612
|
answers=answers or [],
|
|
9090
9613
|
fields=fields or {},
|
|
9091
9614
|
force_refresh_form=force_refresh_form,
|
|
9615
|
+
field_index_override=update_index,
|
|
9092
9616
|
)
|
|
9093
9617
|
self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
|
|
9094
9618
|
try:
|
|
@@ -9142,13 +9666,14 @@ class RecordTools(ToolBase):
|
|
|
9142
9666
|
def record_delete(self, *, profile: str, app_key: str, apply_id: int, list_type: int) -> JSONObject:
|
|
9143
9667
|
"""执行记录相关逻辑。"""
|
|
9144
9668
|
normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
|
|
9669
|
+
delete_list_type = self._resolve_record_delete_list_type(view_id=None, list_type=list_type)
|
|
9145
9670
|
|
|
9146
9671
|
def runner(session_profile, context):
|
|
9147
9672
|
result = self.backend.request(
|
|
9148
9673
|
"DELETE",
|
|
9149
9674
|
context,
|
|
9150
9675
|
f"/app/{app_key}/apply",
|
|
9151
|
-
json_body={"type":
|
|
9676
|
+
json_body={"type": delete_list_type, "applyIds": [normalized_apply_id]},
|
|
9152
9677
|
)
|
|
9153
9678
|
return self._attach_human_review_notice(
|
|
9154
9679
|
{
|
|
@@ -9157,6 +9682,7 @@ class RecordTools(ToolBase):
|
|
|
9157
9682
|
"request_route": self._request_route_payload(context),
|
|
9158
9683
|
"app_key": app_key,
|
|
9159
9684
|
"apply_id": normalized_apply_id,
|
|
9685
|
+
"list_type": delete_list_type,
|
|
9160
9686
|
"result": result,
|
|
9161
9687
|
},
|
|
9162
9688
|
operation="delete",
|
|
@@ -9201,7 +9727,7 @@ class RecordTools(ToolBase):
|
|
|
9201
9727
|
"GET",
|
|
9202
9728
|
context,
|
|
9203
9729
|
f"/app/{app_key}/apply/{apply_id}",
|
|
9204
|
-
params={"role":
|
|
9730
|
+
params={"role": _record_detail_role_for_list_type(list_type), "listType": list_type},
|
|
9205
9731
|
)
|
|
9206
9732
|
answers = result.get("answers") if isinstance(result, dict) else None
|
|
9207
9733
|
answer_list = answers if isinstance(answers, list) else []
|
|
@@ -9560,7 +10086,7 @@ class RecordTools(ToolBase):
|
|
|
9560
10086
|
used_list_type: int | None = None
|
|
9561
10087
|
if view_selection is not None:
|
|
9562
10088
|
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
|
|
10089
|
+
elif view_route.list_type is not None:
|
|
9564
10090
|
fallback_list_types = [view_route.list_type]
|
|
9565
10091
|
else:
|
|
9566
10092
|
fallback_list_types = [DEFAULT_RECORD_LIST_TYPE, 14, 1, 2, 12]
|
|
@@ -9791,7 +10317,7 @@ class RecordTools(ToolBase):
|
|
|
9791
10317
|
try:
|
|
9792
10318
|
payload = self.backend.request("GET", context, f"/view/{view_key}/viewConfig")
|
|
9793
10319
|
except QingflowApiError as exc:
|
|
9794
|
-
if exc
|
|
10320
|
+
if _is_optional_schema_permission_error(exc):
|
|
9795
10321
|
self._view_config_cache[cache_key] = None
|
|
9796
10322
|
return None
|
|
9797
10323
|
raise
|
|
@@ -9912,7 +10438,12 @@ class RecordTools(ToolBase):
|
|
|
9912
10438
|
)
|
|
9913
10439
|
normalized = _normalize_data_list_base_info_schema(payload)
|
|
9914
10440
|
if not isinstance(normalized.get("formQues"), list) or not normalized.get("formQues"):
|
|
9915
|
-
|
|
10441
|
+
try:
|
|
10442
|
+
return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
|
|
10443
|
+
except QingflowApiError as exc:
|
|
10444
|
+
if not _is_optional_schema_permission_error(exc):
|
|
10445
|
+
raise
|
|
10446
|
+
return normalized
|
|
9916
10447
|
self._form_cache[cache_key] = normalized
|
|
9917
10448
|
return normalized
|
|
9918
10449
|
|
|
@@ -9944,8 +10475,16 @@ class RecordTools(ToolBase):
|
|
|
9944
10475
|
cache_key = (profile, f"view:{view_key}", "browse_view_base_info", None)
|
|
9945
10476
|
if not force_refresh and cache_key in self._form_cache:
|
|
9946
10477
|
return self._form_cache[cache_key]
|
|
9947
|
-
|
|
9948
|
-
|
|
10478
|
+
try:
|
|
10479
|
+
payload = self.backend.request("GET", context, f"/view/{view_key}/apply/baseInfo")
|
|
10480
|
+
normalized = _normalize_data_list_base_info_schema(payload)
|
|
10481
|
+
form_ques = normalized.get("formQues")
|
|
10482
|
+
if not isinstance(form_ques, list) or not form_ques:
|
|
10483
|
+
normalized = self._get_view_form_schema(profile, context, view_key, force_refresh=force_refresh)
|
|
10484
|
+
except QingflowApiError as exc:
|
|
10485
|
+
if not _is_optional_schema_permission_error(exc):
|
|
10486
|
+
raise
|
|
10487
|
+
normalized = self._get_view_form_schema(profile, context, view_key, force_refresh=force_refresh)
|
|
9949
10488
|
self._form_cache[cache_key] = normalized
|
|
9950
10489
|
return normalized
|
|
9951
10490
|
|
|
@@ -10001,22 +10540,6 @@ class RecordTools(ToolBase):
|
|
|
10001
10540
|
force_refresh: bool,
|
|
10002
10541
|
) -> JSONObject:
|
|
10003
10542
|
"""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
10543
|
if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
10021
10544
|
schema = self._get_custom_view_browse_schema(
|
|
10022
10545
|
profile,
|
|
@@ -10025,6 +10548,16 @@ class RecordTools(ToolBase):
|
|
|
10025
10548
|
force_refresh=force_refresh,
|
|
10026
10549
|
)
|
|
10027
10550
|
index = _build_top_level_field_index(schema)
|
|
10551
|
+
visible_question_ids = {field.que_id for field in index.by_id.values()}
|
|
10552
|
+
return {
|
|
10553
|
+
"index": index,
|
|
10554
|
+
"writable_field_ids": {
|
|
10555
|
+
field.que_id
|
|
10556
|
+
for field in index.by_id.values()
|
|
10557
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
10558
|
+
},
|
|
10559
|
+
"visible_question_ids": visible_question_ids,
|
|
10560
|
+
}
|
|
10028
10561
|
elif resolved_view is not None and resolved_view.kind == "system" and resolved_view.list_type is not None:
|
|
10029
10562
|
schema = self._get_system_browse_base_info_schema(
|
|
10030
10563
|
profile,
|
|
@@ -10034,34 +10567,26 @@ class RecordTools(ToolBase):
|
|
|
10034
10567
|
force_refresh=force_refresh,
|
|
10035
10568
|
)
|
|
10036
10569
|
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]]})
|
|
10570
|
+
visible_question_ids = {field.que_id for field in index.by_id.values()}
|
|
10571
|
+
return {
|
|
10572
|
+
"index": index,
|
|
10573
|
+
"writable_field_ids": {
|
|
10574
|
+
field.que_id
|
|
10575
|
+
for field in index.by_id.values()
|
|
10576
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
10577
|
+
},
|
|
10578
|
+
"visible_question_ids": visible_question_ids,
|
|
10579
|
+
}
|
|
10048
10580
|
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10581
|
+
applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
|
|
10582
|
+
visible_question_ids = {field.que_id for field in applicant_index.by_id.values()}
|
|
10583
|
+
return {
|
|
10584
|
+
"index": applicant_index,
|
|
10585
|
+
"writable_field_ids": {
|
|
10052
10586
|
field.que_id
|
|
10053
|
-
for field in
|
|
10587
|
+
for field in applicant_index.by_id.values()
|
|
10054
10588
|
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,
|
|
10589
|
+
},
|
|
10065
10590
|
"visible_question_ids": visible_question_ids,
|
|
10066
10591
|
}
|
|
10067
10592
|
|
|
@@ -10075,23 +10600,13 @@ class RecordTools(ToolBase):
|
|
|
10075
10600
|
force_refresh: bool,
|
|
10076
10601
|
) -> JSONObject:
|
|
10077
10602
|
"""执行内部辅助逻辑。"""
|
|
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
10603
|
if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
10094
|
-
schema = self.
|
|
10604
|
+
schema = self._get_custom_view_browse_schema(
|
|
10605
|
+
profile,
|
|
10606
|
+
context,
|
|
10607
|
+
resolved_view.view_selection.view_key,
|
|
10608
|
+
force_refresh=force_refresh,
|
|
10609
|
+
)
|
|
10095
10610
|
index = _build_top_level_field_index(schema)
|
|
10096
10611
|
visible_question_ids = self._get_view_question_ids(profile, context, resolved_view.view_selection.view_key)
|
|
10097
10612
|
if not visible_question_ids:
|
|
@@ -10107,6 +10622,12 @@ class RecordTools(ToolBase):
|
|
|
10107
10622
|
index = _build_top_level_field_index(schema)
|
|
10108
10623
|
visible_question_ids = _question_ids_from_schema(schema)
|
|
10109
10624
|
else:
|
|
10625
|
+
applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
|
|
10626
|
+
applicant_writable_field_ids = {
|
|
10627
|
+
field.que_id
|
|
10628
|
+
for field in applicant_index.by_id.values()
|
|
10629
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
10630
|
+
}
|
|
10110
10631
|
index = applicant_index or _build_top_level_field_index(
|
|
10111
10632
|
self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
|
|
10112
10633
|
)
|
|
@@ -10125,43 +10646,13 @@ class RecordTools(ToolBase):
|
|
|
10125
10646
|
"visible_question_ids": set(visible_question_ids),
|
|
10126
10647
|
}
|
|
10127
10648
|
|
|
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
10649
|
return {
|
|
10163
|
-
"index":
|
|
10164
|
-
"writable_field_ids":
|
|
10650
|
+
"index": index,
|
|
10651
|
+
"writable_field_ids": {
|
|
10652
|
+
field.que_id
|
|
10653
|
+
for field in index.by_id.values()
|
|
10654
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
10655
|
+
},
|
|
10165
10656
|
"visible_question_ids": visible_question_ids,
|
|
10166
10657
|
}
|
|
10167
10658
|
|
|
@@ -10226,7 +10717,7 @@ class RecordTools(ToolBase):
|
|
|
10226
10717
|
try:
|
|
10227
10718
|
payload = self.backend.request("GET", context, f"/view/{view_key}/question")
|
|
10228
10719
|
except QingflowApiError as exc:
|
|
10229
|
-
if exc
|
|
10720
|
+
if _is_record_permission_denied_error(exc):
|
|
10230
10721
|
return set()
|
|
10231
10722
|
raise
|
|
10232
10723
|
if not isinstance(payload, list):
|
|
@@ -10270,7 +10761,7 @@ class RecordTools(ToolBase):
|
|
|
10270
10761
|
)
|
|
10271
10762
|
return True
|
|
10272
10763
|
except QingflowApiError as exc:
|
|
10273
|
-
if exc
|
|
10764
|
+
if _is_record_permission_denied_error(exc):
|
|
10274
10765
|
return False
|
|
10275
10766
|
raise
|
|
10276
10767
|
|
|
@@ -10423,7 +10914,12 @@ class RecordTools(ToolBase):
|
|
|
10423
10914
|
requested_name = _normalize_optional_text(view_name)
|
|
10424
10915
|
if requested_key is None and requested_name is None:
|
|
10425
10916
|
return None
|
|
10426
|
-
|
|
10917
|
+
try:
|
|
10918
|
+
views = self._get_view_list(profile, context, app_key)
|
|
10919
|
+
except QingflowApiError as exc:
|
|
10920
|
+
if requested_key is None or not _is_record_permission_denied_error(exc):
|
|
10921
|
+
raise
|
|
10922
|
+
views = []
|
|
10427
10923
|
selected: JSONObject | None = None
|
|
10428
10924
|
if requested_key is not None:
|
|
10429
10925
|
selected = next((item for item in views if _normalize_optional_text(item.get("viewKey")) == requested_key), None)
|
|
@@ -10621,9 +11117,11 @@ class RecordTools(ToolBase):
|
|
|
10621
11117
|
|
|
10622
11118
|
def _should_retry_list_type_fallback(self, error: QingflowApiError) -> bool:
|
|
10623
11119
|
"""执行内部辅助逻辑。"""
|
|
10624
|
-
if error
|
|
11120
|
+
if is_auth_like_error(error):
|
|
11121
|
+
return False
|
|
11122
|
+
if backend_code_int(error) in {40002, 40027, 404}:
|
|
10625
11123
|
return True
|
|
10626
|
-
if error.http_status
|
|
11124
|
+
if error.http_status == 404:
|
|
10627
11125
|
return True
|
|
10628
11126
|
return False
|
|
10629
11127
|
|
|
@@ -11405,6 +11903,8 @@ class RecordTools(ToolBase):
|
|
|
11405
11903
|
schema: JSONObject = {}
|
|
11406
11904
|
if isinstance(raw.get("subQuestions"), list):
|
|
11407
11905
|
schema["formQues"] = [raw["subQuestions"]]
|
|
11906
|
+
elif isinstance(raw.get("subQues"), list):
|
|
11907
|
+
schema["formQues"] = [raw["subQues"]]
|
|
11408
11908
|
elif isinstance(raw.get("innerQuestions"), list):
|
|
11409
11909
|
schema["formQues"] = raw["innerQuestions"]
|
|
11410
11910
|
index = _build_field_index(schema)
|
|
@@ -11444,6 +11944,70 @@ class RecordTools(ToolBase):
|
|
|
11444
11944
|
)
|
|
11445
11945
|
)
|
|
11446
11946
|
|
|
11947
|
+
def _candidate_lookup_failed_response(
|
|
11948
|
+
self,
|
|
11949
|
+
*,
|
|
11950
|
+
profile: str,
|
|
11951
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
11952
|
+
context, # type: ignore[no-untyped-def]
|
|
11953
|
+
kind: str,
|
|
11954
|
+
error: RecordInputError,
|
|
11955
|
+
field: FormField,
|
|
11956
|
+
app_key: str,
|
|
11957
|
+
record_id_text: str | None,
|
|
11958
|
+
workflow_node_id: int | None,
|
|
11959
|
+
fields_present: bool,
|
|
11960
|
+
keyword: str,
|
|
11961
|
+
scope_source: str,
|
|
11962
|
+
) -> JSONObject:
|
|
11963
|
+
"""Return a structured result when an optional field candidate lookup is unavailable."""
|
|
11964
|
+
error_payload = error.to_dict()
|
|
11965
|
+
error_details = error_payload.get("details") if isinstance(error_payload.get("details"), dict) else {}
|
|
11966
|
+
candidate_error = error_details.get("candidate_error") if isinstance(error_details.get("candidate_error"), dict) else {}
|
|
11967
|
+
warning_transport = {
|
|
11968
|
+
key: candidate_error.get(key)
|
|
11969
|
+
for key in ("backend_code", "http_status", "request_id")
|
|
11970
|
+
if candidate_error.get(key) is not None
|
|
11971
|
+
}
|
|
11972
|
+
selection: JSONObject = {
|
|
11973
|
+
"app_key": app_key,
|
|
11974
|
+
"field_id": field.que_id,
|
|
11975
|
+
"field_title": field.que_title,
|
|
11976
|
+
"record_id": record_id_text,
|
|
11977
|
+
"workflow_node_id": workflow_node_id,
|
|
11978
|
+
"fields_present": fields_present,
|
|
11979
|
+
"keyword": keyword,
|
|
11980
|
+
"permission_scope": "applicant_node",
|
|
11981
|
+
}
|
|
11982
|
+
return {
|
|
11983
|
+
"profile": profile,
|
|
11984
|
+
"ws_id": session_profile.selected_ws_id,
|
|
11985
|
+
"ok": False,
|
|
11986
|
+
"status": "failed",
|
|
11987
|
+
"error_code": error.error_code,
|
|
11988
|
+
"message": error.message,
|
|
11989
|
+
"request_route": self._request_route_payload(context),
|
|
11990
|
+
"warnings": [
|
|
11991
|
+
{
|
|
11992
|
+
"code": error.error_code,
|
|
11993
|
+
"message": error.fix_hint,
|
|
11994
|
+
"kind": kind,
|
|
11995
|
+
"field_id": field.que_id,
|
|
11996
|
+
"field_title": field.que_title,
|
|
11997
|
+
**warning_transport,
|
|
11998
|
+
}
|
|
11999
|
+
],
|
|
12000
|
+
"output_profile": "normal",
|
|
12001
|
+
"data": {
|
|
12002
|
+
"items": [],
|
|
12003
|
+
"pagination": {"returned_items": 0},
|
|
12004
|
+
"selection": selection,
|
|
12005
|
+
"scope_source": scope_source,
|
|
12006
|
+
"fix_hint": error.fix_hint,
|
|
12007
|
+
},
|
|
12008
|
+
"details": error_details,
|
|
12009
|
+
}
|
|
12010
|
+
|
|
11447
12011
|
def _request_route_payload(self, context) -> JSONObject: # type: ignore[no-untyped-def]
|
|
11448
12012
|
"""执行内部辅助逻辑。"""
|
|
11449
12013
|
describe_route = getattr(self.backend, "describe_route", None)
|
|
@@ -11617,7 +12181,7 @@ class RecordTools(ToolBase):
|
|
|
11617
12181
|
selection: JSONObject | None,
|
|
11618
12182
|
) -> None:
|
|
11619
12183
|
"""执行内部辅助逻辑。"""
|
|
11620
|
-
if exc
|
|
12184
|
+
if not _is_record_permission_denied_error(exc):
|
|
11621
12185
|
raise exc
|
|
11622
12186
|
raise_tool_error(
|
|
11623
12187
|
QingflowApiError(
|
|
@@ -11783,6 +12347,7 @@ class RecordTools(ToolBase):
|
|
|
11783
12347
|
response_status = raw_status or "failed"
|
|
11784
12348
|
update_route = raw_apply.get("update_route") if isinstance(raw_apply.get("update_route"), dict) else None
|
|
11785
12349
|
tried_routes = raw_apply.get("tried_routes") if isinstance(raw_apply.get("tried_routes"), list) else []
|
|
12350
|
+
expose_tried_routes = output_profile == "verbose" or response_status != "success"
|
|
11786
12351
|
response: JSONObject = {
|
|
11787
12352
|
"profile": raw_apply.get("profile"),
|
|
11788
12353
|
"ws_id": raw_apply.get("ws_id"),
|
|
@@ -11795,7 +12360,6 @@ class RecordTools(ToolBase):
|
|
|
11795
12360
|
"warnings": warnings,
|
|
11796
12361
|
"output_profile": output_profile,
|
|
11797
12362
|
"update_route": update_route,
|
|
11798
|
-
"tried_routes": tried_routes,
|
|
11799
12363
|
"data": {
|
|
11800
12364
|
"action": {"operation": operation, "executed": True},
|
|
11801
12365
|
"resource": resource,
|
|
@@ -11807,9 +12371,11 @@ class RecordTools(ToolBase):
|
|
|
11807
12371
|
"resolved_fields": resolved_fields,
|
|
11808
12372
|
"human_review": self._record_write_human_review_payload(operation, enabled=human_review),
|
|
11809
12373
|
"update_route": update_route,
|
|
11810
|
-
"tried_routes": tried_routes,
|
|
11811
12374
|
},
|
|
11812
12375
|
}
|
|
12376
|
+
if expose_tried_routes:
|
|
12377
|
+
response["tried_routes"] = tried_routes
|
|
12378
|
+
response["data"]["tried_routes"] = tried_routes
|
|
11813
12379
|
if record_id is not None:
|
|
11814
12380
|
response["record_id"] = record_id
|
|
11815
12381
|
if apply_id is not None:
|
|
@@ -11989,7 +12555,7 @@ class RecordTools(ToolBase):
|
|
|
11989
12555
|
)
|
|
11990
12556
|
return errors
|
|
11991
12557
|
|
|
11992
|
-
def _record_delete_many(self, *, profile: str, app_key: str, record_ids: list[int]) -> JSONObject:
|
|
12558
|
+
def _record_delete_many(self, *, profile: str, app_key: str, record_ids: list[int], list_type: int = DEFAULT_RECORD_LIST_TYPE) -> JSONObject:
|
|
11993
12559
|
"""执行内部辅助逻辑。"""
|
|
11994
12560
|
if not app_key:
|
|
11995
12561
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
@@ -12002,14 +12568,14 @@ class RecordTools(ToolBase):
|
|
|
12002
12568
|
"DELETE",
|
|
12003
12569
|
context,
|
|
12004
12570
|
f"/app/{app_key}/apply",
|
|
12005
|
-
json_body={"type":
|
|
12571
|
+
json_body={"type": list_type, "applyIds": normalized_ids},
|
|
12006
12572
|
)
|
|
12007
12573
|
return {
|
|
12008
12574
|
"profile": profile,
|
|
12009
12575
|
"ws_id": session_profile.selected_ws_id,
|
|
12010
12576
|
"request_route": self._request_route_payload(context),
|
|
12011
12577
|
"result": result,
|
|
12012
|
-
"resource": {"type": "record", "apply_ids": normalized_ids},
|
|
12578
|
+
"resource": {"type": "record", "apply_ids": normalized_ids, "list_type": list_type},
|
|
12013
12579
|
"ok": True,
|
|
12014
12580
|
}
|
|
12015
12581
|
|
|
@@ -12583,6 +13149,30 @@ class RecordTools(ToolBase):
|
|
|
12583
13149
|
},
|
|
12584
13150
|
)
|
|
12585
13151
|
|
|
13152
|
+
def _candidate_lookup_error(
|
|
13153
|
+
self,
|
|
13154
|
+
*,
|
|
13155
|
+
kind: str,
|
|
13156
|
+
field: FormField,
|
|
13157
|
+
value: JSONValue,
|
|
13158
|
+
error: QingflowApiError,
|
|
13159
|
+
) -> RecordInputError:
|
|
13160
|
+
"""Build the standard candidate lookup failure without raising it."""
|
|
13161
|
+
field_kind = "member" if kind == "member" else "department"
|
|
13162
|
+
return RecordInputError(
|
|
13163
|
+
message=f"{field_kind} candidates for field '{field.que_title}' could not be loaded",
|
|
13164
|
+
error_code=f"{kind.upper()}_CANDIDATE_LOOKUP_FAILED",
|
|
13165
|
+
fix_hint=(
|
|
13166
|
+
f"Run record_{field_kind}_candidates again after the backend error is resolved, "
|
|
13167
|
+
"then choose one returned item exactly."
|
|
13168
|
+
),
|
|
13169
|
+
details={
|
|
13170
|
+
"field": _field_ref_payload(field),
|
|
13171
|
+
"received_value": value,
|
|
13172
|
+
"candidate_error": error.to_dict(),
|
|
13173
|
+
},
|
|
13174
|
+
)
|
|
13175
|
+
|
|
12586
13176
|
def _candidate_keyword_from_value(
|
|
12587
13177
|
self,
|
|
12588
13178
|
value: JSONValue,
|
|
@@ -12847,14 +13437,7 @@ class RecordTools(ToolBase):
|
|
|
12847
13437
|
|
|
12848
13438
|
def _lookup_department_detail(self, context, keyword: str, *, purpose: str) -> JSONObject: # type: ignore[no-untyped-def]
|
|
12849
13439
|
"""执行内部辅助逻辑。"""
|
|
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 []
|
|
13440
|
+
items = self._search_workspace_departments(context, keyword=keyword)
|
|
12858
13441
|
normalized_keyword = keyword.strip()
|
|
12859
13442
|
exact = [
|
|
12860
13443
|
item for item in items
|
|
@@ -13379,6 +13962,7 @@ class RecordTools(ToolBase):
|
|
|
13379
13962
|
normalized_answers: list[JSONObject],
|
|
13380
13963
|
index: FieldIndex,
|
|
13381
13964
|
verify_list_type: int = DEFAULT_RECORD_LIST_TYPE,
|
|
13965
|
+
verify_role: int | None = None,
|
|
13382
13966
|
verify_view_key: str | None = None,
|
|
13383
13967
|
) -> JSONObject:
|
|
13384
13968
|
"""执行内部辅助逻辑。"""
|
|
@@ -13398,14 +13982,20 @@ class RecordTools(ToolBase):
|
|
|
13398
13982
|
f"/view/{verify_view_key}/apply/{apply_id}",
|
|
13399
13983
|
)
|
|
13400
13984
|
else:
|
|
13985
|
+
role = verify_role if verify_role is not None else 1
|
|
13401
13986
|
record = self.backend.request(
|
|
13402
13987
|
"GET",
|
|
13403
13988
|
context,
|
|
13404
13989
|
f"/app/{app_key}/apply/{apply_id}",
|
|
13405
|
-
params={"role":
|
|
13990
|
+
params={"role": role, "listType": verify_list_type},
|
|
13406
13991
|
)
|
|
13407
13992
|
except QingflowApiError as exc:
|
|
13408
13993
|
if verify_view_key:
|
|
13994
|
+
warning: JSONObject = {
|
|
13995
|
+
"code": "WRITE_VERIFY_CUSTOM_VIEW_READBACK_FAILED",
|
|
13996
|
+
"message": "Write was sent through a custom view route, but the same view could not be re-read for field-level verification.",
|
|
13997
|
+
}
|
|
13998
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
13409
13999
|
return {
|
|
13410
14000
|
"verified": False,
|
|
13411
14001
|
"verification_mode": "custom_view_record_detail",
|
|
@@ -13414,14 +14004,9 @@ class RecordTools(ToolBase):
|
|
|
13414
14004
|
"missing_fields": [],
|
|
13415
14005
|
"empty_fields": [],
|
|
13416
14006
|
"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
|
-
}],
|
|
14007
|
+
"warnings": [warning],
|
|
13423
14008
|
}
|
|
13424
|
-
if exc
|
|
14009
|
+
if not _is_record_permission_denied_error(exc):
|
|
13425
14010
|
raise
|
|
13426
14011
|
return self._verify_record_write_result_via_initiated_tasks(
|
|
13427
14012
|
context,
|
|
@@ -13469,6 +14054,7 @@ class RecordTools(ToolBase):
|
|
|
13469
14054
|
or len(count_mismatches) > mismatch_before
|
|
13470
14055
|
):
|
|
13471
14056
|
continue
|
|
14057
|
+
continue
|
|
13472
14058
|
expected_value = _canonicalize_answer_value_for_compare(answer, field)
|
|
13473
14059
|
actual_value = _canonicalize_answer_value_for_compare(actual, field)
|
|
13474
14060
|
if not _canonical_value_is_empty(expected_value) and _canonical_value_is_empty(actual_value):
|
|
@@ -13715,6 +14301,8 @@ def _normalize_data_list_base_info_schema(payload: JSONValue) -> JSONObject:
|
|
|
13715
14301
|
if not isinstance(payload, dict):
|
|
13716
14302
|
return {}
|
|
13717
14303
|
que_base_infos = payload.get("queBaseInfos")
|
|
14304
|
+
if not isinstance(que_base_infos, list) and isinstance(payload.get("formQues"), list):
|
|
14305
|
+
que_base_infos = payload.get("formQues")
|
|
13718
14306
|
if not isinstance(que_base_infos, list):
|
|
13719
14307
|
return {}
|
|
13720
14308
|
return {
|
|
@@ -14101,6 +14689,44 @@ def _build_answer_backed_field_index(
|
|
|
14101
14689
|
)
|
|
14102
14690
|
|
|
14103
14691
|
|
|
14692
|
+
def _merge_subtable_parent_field(primary: FormField, extra: FormField) -> FormField:
|
|
14693
|
+
if primary.que_type not in SUBTABLE_QUE_TYPES or extra.que_type not in SUBTABLE_QUE_TYPES:
|
|
14694
|
+
return primary
|
|
14695
|
+
primary_raw = dict(primary.raw) if isinstance(primary.raw, dict) else {}
|
|
14696
|
+
extra_raw = dict(extra.raw) if isinstance(extra.raw, dict) else {}
|
|
14697
|
+
primary_subquestions = primary_raw.get("subQuestions")
|
|
14698
|
+
extra_subquestions = extra_raw.get("subQuestions")
|
|
14699
|
+
if not isinstance(primary_subquestions, list) or not isinstance(extra_subquestions, list):
|
|
14700
|
+
return primary
|
|
14701
|
+
merged_subquestions = [item for item in primary_subquestions if isinstance(item, dict)]
|
|
14702
|
+
seen_ids = {
|
|
14703
|
+
_coerce_count(item.get("queId"))
|
|
14704
|
+
for item in merged_subquestions
|
|
14705
|
+
if isinstance(item, dict) and _coerce_count(item.get("queId")) is not None
|
|
14706
|
+
}
|
|
14707
|
+
for item in extra_subquestions:
|
|
14708
|
+
if not isinstance(item, dict):
|
|
14709
|
+
continue
|
|
14710
|
+
que_id = _coerce_count(item.get("queId"))
|
|
14711
|
+
if que_id is not None and que_id in seen_ids:
|
|
14712
|
+
continue
|
|
14713
|
+
merged_subquestions.append(item)
|
|
14714
|
+
if que_id is not None:
|
|
14715
|
+
seen_ids.add(que_id)
|
|
14716
|
+
if len(merged_subquestions) == len(primary_subquestions):
|
|
14717
|
+
return primary
|
|
14718
|
+
merged_raw = dict(primary_raw)
|
|
14719
|
+
merged_raw["subQuestions"] = merged_subquestions
|
|
14720
|
+
merged_field = _clone_form_field(primary)
|
|
14721
|
+
merged_field.raw = merged_raw
|
|
14722
|
+
return merged_field
|
|
14723
|
+
|
|
14724
|
+
|
|
14725
|
+
def _replace_field_in_lookup(index: dict[str, list[FormField]], field: FormField) -> None:
|
|
14726
|
+
for key, fields in list(index.items()):
|
|
14727
|
+
index[key] = [field if existing.que_id == field.que_id else existing for existing in fields]
|
|
14728
|
+
|
|
14729
|
+
|
|
14104
14730
|
def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
|
|
14105
14731
|
by_id = dict(primary.by_id)
|
|
14106
14732
|
by_title = {key: list(value) for key, value in primary.by_title.items()}
|
|
@@ -14111,12 +14737,42 @@ def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
|
|
|
14111
14737
|
|
|
14112
14738
|
for field_id, field in extra.by_id.items():
|
|
14113
14739
|
if field_id in by_id:
|
|
14740
|
+
merged_field = _merge_subtable_parent_field(by_id[field_id], field)
|
|
14741
|
+
if merged_field is not by_id[field_id]:
|
|
14742
|
+
by_id[field_id] = merged_field
|
|
14743
|
+
_replace_field_in_lookup(by_title, merged_field)
|
|
14744
|
+
_replace_field_in_lookup(by_alias, merged_field)
|
|
14114
14745
|
continue
|
|
14115
14746
|
by_id[field_id] = field
|
|
14116
14747
|
by_title.setdefault(_normalize_field_lookup_key(field.que_title), []).append(field)
|
|
14117
14748
|
for alias in field.aliases:
|
|
14118
14749
|
by_alias.setdefault(_normalize_field_lookup_key(alias), []).append(field)
|
|
14119
14750
|
|
|
14751
|
+
for field_id, fields in extra.subtable_leaf_by_id.items():
|
|
14752
|
+
merged = subtable_leaf_by_id.setdefault(field_id, [])
|
|
14753
|
+
existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
|
|
14754
|
+
for field in fields:
|
|
14755
|
+
key = (field.field.que_id, field.parent_field.que_id)
|
|
14756
|
+
if key not in existing:
|
|
14757
|
+
merged.append(field)
|
|
14758
|
+
existing.add(key)
|
|
14759
|
+
for title, fields in extra.subtable_leaf_by_title.items():
|
|
14760
|
+
merged = subtable_leaf_by_title.setdefault(title, [])
|
|
14761
|
+
existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
|
|
14762
|
+
for field in fields:
|
|
14763
|
+
key = (field.field.que_id, field.parent_field.que_id)
|
|
14764
|
+
if key not in existing:
|
|
14765
|
+
merged.append(field)
|
|
14766
|
+
existing.add(key)
|
|
14767
|
+
for alias, fields in extra.subtable_leaf_by_alias.items():
|
|
14768
|
+
merged = subtable_leaf_by_alias.setdefault(alias, [])
|
|
14769
|
+
existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
|
|
14770
|
+
for field in fields:
|
|
14771
|
+
key = (field.field.que_id, field.parent_field.que_id)
|
|
14772
|
+
if key not in existing:
|
|
14773
|
+
merged.append(field)
|
|
14774
|
+
existing.add(key)
|
|
14775
|
+
|
|
14120
14776
|
return FieldIndex(
|
|
14121
14777
|
by_id=by_id,
|
|
14122
14778
|
by_title=by_title,
|
|
@@ -15507,6 +16163,28 @@ def _record_detail_unavailable_context(section: str, message: str, exc: Qingflow
|
|
|
15507
16163
|
"message": message,
|
|
15508
16164
|
"category": exc.category,
|
|
15509
16165
|
}
|
|
16166
|
+
if is_auth_like_error(exc):
|
|
16167
|
+
payload["auth_like"] = True
|
|
16168
|
+
payload["error_code"] = "AUTH_REQUIRED"
|
|
16169
|
+
if exc.backend_code is not None:
|
|
16170
|
+
payload["backend_code"] = exc.backend_code
|
|
16171
|
+
if exc.http_status is not None:
|
|
16172
|
+
payload["http_status"] = exc.http_status
|
|
16173
|
+
request_id = getattr(exc, "request_id", None)
|
|
16174
|
+
if request_id:
|
|
16175
|
+
payload["request_id"] = request_id
|
|
16176
|
+
details = exc.details if isinstance(exc.details, dict) else {}
|
|
16177
|
+
error_code = details.get("error_code")
|
|
16178
|
+
if error_code and not payload.get("error_code"):
|
|
16179
|
+
payload["error_code"] = error_code
|
|
16180
|
+
return payload
|
|
16181
|
+
|
|
16182
|
+
|
|
16183
|
+
def _record_detail_error_warning_fields(exc: QingflowApiError) -> JSONObject:
|
|
16184
|
+
payload: JSONObject = {"category": exc.category}
|
|
16185
|
+
if is_auth_like_error(exc):
|
|
16186
|
+
payload["auth_like"] = True
|
|
16187
|
+
payload["error_code"] = "AUTH_REQUIRED"
|
|
15510
16188
|
if exc.backend_code is not None:
|
|
15511
16189
|
payload["backend_code"] = exc.backend_code
|
|
15512
16190
|
if exc.http_status is not None:
|
|
@@ -15516,11 +16194,35 @@ def _record_detail_unavailable_context(section: str, message: str, exc: Qingflow
|
|
|
15516
16194
|
payload["request_id"] = request_id
|
|
15517
16195
|
details = exc.details if isinstance(exc.details, dict) else {}
|
|
15518
16196
|
error_code = details.get("error_code")
|
|
15519
|
-
if error_code:
|
|
16197
|
+
if error_code and not payload.get("error_code"):
|
|
15520
16198
|
payload["error_code"] = error_code
|
|
15521
16199
|
return payload
|
|
15522
16200
|
|
|
15523
16201
|
|
|
16202
|
+
def _record_detail_refreshed_source_url(refresh_result: Any) -> str | None:
|
|
16203
|
+
if isinstance(refresh_result, dict):
|
|
16204
|
+
return _normalize_optional_text(refresh_result.get("source_url"))
|
|
16205
|
+
return _normalize_optional_text(refresh_result)
|
|
16206
|
+
|
|
16207
|
+
|
|
16208
|
+
def _record_detail_append_refresh_warning(
|
|
16209
|
+
warnings: list[JSONObject],
|
|
16210
|
+
refresh_result: Any,
|
|
16211
|
+
*,
|
|
16212
|
+
id_key: str,
|
|
16213
|
+
id_value: str,
|
|
16214
|
+
) -> None:
|
|
16215
|
+
if not isinstance(refresh_result, dict):
|
|
16216
|
+
return
|
|
16217
|
+
warning = refresh_result.get("warning")
|
|
16218
|
+
if not isinstance(warning, dict):
|
|
16219
|
+
return
|
|
16220
|
+
payload: JSONObject = dict(warning)
|
|
16221
|
+
payload.setdefault("code", "ASSET_STORAGE_URL_REFRESH_FAILED")
|
|
16222
|
+
payload.setdefault(id_key, id_value)
|
|
16223
|
+
warnings.append(payload)
|
|
16224
|
+
|
|
16225
|
+
|
|
15524
16226
|
_RECORD_MEDIA_IMG_SRC_RE = re.compile(r"""<img\b[^>]*\bsrc\s*=\s*["']?([^"'\s>]+)""", re.IGNORECASE)
|
|
15525
16227
|
_RECORD_MEDIA_MD_IMAGE_RE = re.compile(r"""!\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)""")
|
|
15526
16228
|
_RECORD_MEDIA_URL_RE = re.compile(r"""https?://[^\s<>"')\]]+""", re.IGNORECASE)
|
|
@@ -15675,7 +16377,14 @@ def _record_detail_media_assets_payload(
|
|
|
15675
16377
|
except QingflowApiError as exc:
|
|
15676
16378
|
blocked = exc.http_status in {401, 403}
|
|
15677
16379
|
if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
|
|
15678
|
-
|
|
16380
|
+
refresh_result = refresh_source_url(candidate)
|
|
16381
|
+
_record_detail_append_refresh_warning(
|
|
16382
|
+
warnings,
|
|
16383
|
+
refresh_result,
|
|
16384
|
+
id_key="asset_id",
|
|
16385
|
+
id_value=asset_id,
|
|
16386
|
+
)
|
|
16387
|
+
refreshed_url = _record_detail_refreshed_source_url(refresh_result)
|
|
15679
16388
|
if refreshed_url and refreshed_url != source_url:
|
|
15680
16389
|
refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
|
|
15681
16390
|
try:
|
|
@@ -15717,14 +16426,13 @@ def _record_detail_media_assets_payload(
|
|
|
15717
16426
|
"readable_by_agent": False,
|
|
15718
16427
|
}
|
|
15719
16428
|
)
|
|
15720
|
-
|
|
15721
|
-
|
|
15722
|
-
|
|
15723
|
-
|
|
15724
|
-
|
|
15725
|
-
|
|
15726
|
-
|
|
15727
|
-
)
|
|
16429
|
+
warning: JSONObject = {
|
|
16430
|
+
"code": warning_code,
|
|
16431
|
+
"asset_id": asset_id,
|
|
16432
|
+
"message": f"record_get could not download image asset {asset_id}: {exc.message}",
|
|
16433
|
+
}
|
|
16434
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
16435
|
+
warnings.append(warning)
|
|
15728
16436
|
continue
|
|
15729
16437
|
|
|
15730
16438
|
if not isinstance(content, bytes):
|
|
@@ -15942,7 +16650,14 @@ def _record_detail_file_assets_payload(
|
|
|
15942
16650
|
except QingflowApiError as exc:
|
|
15943
16651
|
blocked = exc.http_status in {401, 403}
|
|
15944
16652
|
if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
|
|
15945
|
-
|
|
16653
|
+
refresh_result = refresh_source_url(candidate)
|
|
16654
|
+
_record_detail_append_refresh_warning(
|
|
16655
|
+
warnings,
|
|
16656
|
+
refresh_result,
|
|
16657
|
+
id_key="file_asset_id",
|
|
16658
|
+
id_value=file_asset_id,
|
|
16659
|
+
)
|
|
16660
|
+
refreshed_url = _record_detail_refreshed_source_url(refresh_result)
|
|
15946
16661
|
if refreshed_url and refreshed_url != source_url:
|
|
15947
16662
|
refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
|
|
15948
16663
|
try:
|
|
@@ -15989,14 +16704,13 @@ def _record_detail_file_assets_payload(
|
|
|
15989
16704
|
"extraction": {"status": "failed", "text_path": None, "preview": None},
|
|
15990
16705
|
}
|
|
15991
16706
|
)
|
|
15992
|
-
|
|
15993
|
-
|
|
15994
|
-
|
|
15995
|
-
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
)
|
|
16707
|
+
warning = {
|
|
16708
|
+
"code": warning_code,
|
|
16709
|
+
"file_asset_id": file_asset_id,
|
|
16710
|
+
"message": f"record_get could not download file asset {file_asset_id}: {exc.message}",
|
|
16711
|
+
}
|
|
16712
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
16713
|
+
warnings.append(warning)
|
|
16000
16714
|
continue
|
|
16001
16715
|
|
|
16002
16716
|
if not isinstance(content, bytes):
|
|
@@ -18115,12 +18829,14 @@ def _extract_sort_selector(item: JSONObject) -> JSONValue:
|
|
|
18115
18829
|
return None
|
|
18116
18830
|
|
|
18117
18831
|
|
|
18118
|
-
def _normalize_public_column_selectors(columns: list[JSONObject | int]) -> list[int]:
|
|
18832
|
+
def _normalize_public_column_selectors(columns: list[JSONObject | int | str]) -> list[int]:
|
|
18119
18833
|
normalized: list[int] = []
|
|
18120
18834
|
for item in columns:
|
|
18121
18835
|
field_id: int | None = None
|
|
18122
18836
|
if isinstance(item, int):
|
|
18123
18837
|
field_id = item
|
|
18838
|
+
elif isinstance(item, str):
|
|
18839
|
+
field_id = _coerce_count(item)
|
|
18124
18840
|
elif isinstance(item, dict):
|
|
18125
18841
|
_ensure_allowed_record_list_keys(
|
|
18126
18842
|
item,
|
|
@@ -18132,19 +18848,21 @@ def _normalize_public_column_selectors(columns: list[JSONObject | int]) -> list[
|
|
|
18132
18848
|
if field_id is None or field_id < 0:
|
|
18133
18849
|
raise_tool_error(
|
|
18134
18850
|
QingflowApiError.config_error(
|
|
18135
|
-
"columns must be a list of field_id integers or {field_id} objects"
|
|
18851
|
+
"columns must be a list of field_id integers, integer strings, or {field_id} objects"
|
|
18136
18852
|
)
|
|
18137
18853
|
)
|
|
18138
18854
|
normalized.append(field_id)
|
|
18139
18855
|
return normalized
|
|
18140
18856
|
|
|
18141
18857
|
|
|
18142
|
-
def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int]) -> list[int]:
|
|
18858
|
+
def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int | str]) -> list[int]:
|
|
18143
18859
|
normalized: list[int] = []
|
|
18144
18860
|
for item in query_fields:
|
|
18145
18861
|
field_id: int | None = None
|
|
18146
18862
|
if isinstance(item, int):
|
|
18147
18863
|
field_id = item
|
|
18864
|
+
elif isinstance(item, str):
|
|
18865
|
+
field_id = _coerce_count(item)
|
|
18148
18866
|
elif isinstance(item, dict):
|
|
18149
18867
|
_ensure_allowed_record_list_keys(
|
|
18150
18868
|
item,
|
|
@@ -18156,7 +18874,7 @@ def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int]
|
|
|
18156
18874
|
if field_id is None or field_id < 0:
|
|
18157
18875
|
raise_tool_error(
|
|
18158
18876
|
QingflowApiError.config_error(
|
|
18159
|
-
"query_fields must be a list of field_id integers or {field_id} objects"
|
|
18877
|
+
"query_fields must be a list of field_id integers, integer strings, or {field_id} objects"
|
|
18160
18878
|
)
|
|
18161
18879
|
)
|
|
18162
18880
|
normalized.append(field_id)
|