@josephyan/qingflow-app-user-mcp 1.1.25 → 1.1.26

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.
Files changed (113) hide show
  1. package/README.md +7 -7
  2. package/docs/local-agent-install.md +6 -57
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-app-user-mcp.mjs +33 -2
  5. package/npm/lib/runtime.mjs +101 -21
  6. package/npm/scripts/postinstall.mjs +10 -1
  7. package/package.json +2 -3
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +277 -0
  10. package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
  11. package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
  12. package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
  13. package/skills/qingflow-app-builder/references/create-app.md +149 -0
  14. package/skills/qingflow-app-builder/references/environments.md +63 -0
  15. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  16. package/skills/qingflow-app-builder/references/gotchas.md +108 -0
  17. package/skills/qingflow-app-builder/references/match-rules.md +114 -0
  18. package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
  19. package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
  20. package/skills/qingflow-app-builder/references/tool-selection.md +107 -0
  21. package/skills/qingflow-app-builder/references/update-flow.md +186 -0
  22. package/skills/qingflow-app-builder/references/update-layout.md +68 -0
  23. package/skills/qingflow-app-builder/references/update-schema.md +72 -0
  24. package/skills/qingflow-app-builder/references/update-views.md +291 -0
  25. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  26. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  27. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  28. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  29. package/skills/qingflow-app-user/SKILL.md +14 -17
  30. package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
  31. package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
  32. package/skills/qingflow-app-user/references/record-patterns.md +5 -5
  33. package/skills/qingflow-app-user/references/workflow-usage.md +5 -4
  34. package/skills/qingflow-mcp-setup/SKILL.md +9 -13
  35. package/skills/qingflow-mcp-setup/references/claude-desktop.md +1 -1
  36. package/skills/qingflow-mcp-setup/references/environments.md +1 -2
  37. package/skills/qingflow-mcp-setup/references/generic-stdio.md +5 -6
  38. package/skills/qingflow-record-analysis/SKILL.md +8 -9
  39. package/skills/qingflow-record-delete/SKILL.md +3 -7
  40. package/skills/qingflow-record-import/SKILL.md +2 -35
  41. package/skills/qingflow-record-insert/SKILL.md +6 -78
  42. package/skills/qingflow-record-update/SKILL.md +24 -39
  43. package/skills/qingflow-task-ops/SKILL.md +27 -30
  44. package/skills/qingflow-task-ops/references/environments.md +1 -0
  45. package/skills/qingflow-task-ops/references/workflow-usage.md +6 -4
  46. package/skills/qingflow-workflow-builder/SKILL.md +96 -0
  47. package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
  48. package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
  49. package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
  50. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
  51. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
  52. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
  53. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
  54. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
  55. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
  56. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
  57. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
  58. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
  59. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
  60. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
  61. package/src/qingflow_mcp/__init__.py +1 -1
  62. package/src/qingflow_mcp/__main__.py +2 -6
  63. package/src/qingflow_mcp/builder_facade/models.py +108 -485
  64. package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
  65. package/src/qingflow_mcp/cli/commands/builder.py +364 -322
  66. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  67. package/src/qingflow_mcp/cli/commands/common.py +3 -12
  68. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  69. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  70. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  71. package/src/qingflow_mcp/cli/commands/record.py +27 -101
  72. package/src/qingflow_mcp/cli/commands/task.py +47 -28
  73. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  74. package/src/qingflow_mcp/cli/context.py +3 -0
  75. package/src/qingflow_mcp/cli/formatters.py +16 -784
  76. package/src/qingflow_mcp/cli/main.py +33 -117
  77. package/src/qingflow_mcp/errors.py +2 -43
  78. package/src/qingflow_mcp/public_surface.py +17 -26
  79. package/src/qingflow_mcp/response_trim.py +17 -81
  80. package/src/qingflow_mcp/server.py +12 -14
  81. package/src/qingflow_mcp/server_app_builder.py +36 -79
  82. package/src/qingflow_mcp/server_app_user.py +16 -22
  83. package/src/qingflow_mcp/session_store.py +7 -11
  84. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  85. package/src/qingflow_mcp/solution/executor.py +18 -245
  86. package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
  87. package/src/qingflow_mcp/tools/app_tools.py +43 -184
  88. package/src/qingflow_mcp/tools/approval_tools.py +35 -197
  89. package/src/qingflow_mcp/tools/auth_tools.py +16 -92
  90. package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
  91. package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
  92. package/src/qingflow_mcp/tools/directory_tools.py +72 -236
  93. package/src/qingflow_mcp/tools/export_tools.py +34 -244
  94. package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
  95. package/src/qingflow_mcp/tools/file_tools.py +3 -9
  96. package/src/qingflow_mcp/tools/import_tools.py +49 -336
  97. package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
  98. package/src/qingflow_mcp/tools/package_tools.py +6 -118
  99. package/src/qingflow_mcp/tools/portal_tools.py +3 -39
  100. package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
  101. package/src/qingflow_mcp/tools/record_tools.py +360 -1145
  102. package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
  103. package/src/qingflow_mcp/tools/role_tools.py +9 -80
  104. package/src/qingflow_mcp/tools/solution_tools.py +45 -59
  105. package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
  106. package/src/qingflow_mcp/tools/task_tools.py +29 -113
  107. package/src/qingflow_mcp/tools/view_tools.py +3 -106
  108. package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
  109. package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
  110. package/npm/bin/qingflow-skills.mjs +0 -5
  111. package/skills/qingflow-record-analysis/manifest.yaml +0 -10
  112. package/skills/qingflow-record-insert/manifest.yaml +0 -6
  113. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -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, Callable, cast
17
+ from typing import Any, 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, backend_code_int, is_auth_like_error, raise_tool_error
25
+ from ..errors import QingflowApiError, 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 (
@@ -75,22 +75,6 @@ MAX_SUMMARY_PREVIEW_COLUMN_LIMIT = 6
75
75
  VERIFY_TASK_FALLBACK_PAGE_SIZE = 50
76
76
  VERIFY_TASK_FALLBACK_MAX_PAGES = 3
77
77
  BACKEND_LIST_SEARCH_FIELD_LIMIT = 10
78
- RECORD_WRITE_SYSTEM_FIELD_NAMES = {
79
- "数据ID",
80
- "编号",
81
- "申请人",
82
- "申请时间",
83
- "创建人",
84
- "创建时间",
85
- "提交人",
86
- "提交时间",
87
- "更新时间",
88
- "更新人",
89
- "当前流程状态",
90
- "当前处理人",
91
- "当前处理节点",
92
- "流程标题",
93
- }
94
78
  LOOKUP_RESOLUTION_MIN_SCORE = 0.92
95
79
  LOOKUP_RESOLUTION_MIN_MARGIN = 0.08
96
80
  LOOKUP_CONFIRMATION_CANDIDATE_LIMIT = 5
@@ -147,27 +131,6 @@ SCHEMA_LINKAGE_REFERENCE_SOURCE_MESSAGE = "updating this field may auto-fill or
147
131
  SCHEMA_LINKAGE_REFERENCE_TARGET_MESSAGE = "this field is usually filled from an upstream reference selection or default matching logic"
148
132
  SCHEMA_LINKAGE_REFERENCE_BOTH_MESSAGE = "this field participates in reference-driven auto-fill logic"
149
133
  SCHEMA_LINKAGE_FORMULA_MESSAGE = "this field is usually derived by formula or default auto-fill logic"
150
- OPTIONAL_SCHEMA_PERMISSION_CODES = {40002, 40027, 404}
151
- RECORD_PERMISSION_DENIED_CODES = {40002, 40027}
152
- SYSTEM_VIEW_LIST_TYPES = {int(list_type) for _view_id, list_type, _name in SYSTEM_VIEW_DEFINITIONS}
153
-
154
-
155
- def _is_optional_schema_permission_error(error: QingflowApiError) -> bool:
156
- if is_auth_like_error(error):
157
- return False
158
- return backend_code_int(error) in OPTIONAL_SCHEMA_PERMISSION_CODES or error.http_status == 404
159
-
160
-
161
- def _is_record_permission_denied_error(error: QingflowApiError) -> bool:
162
- if is_auth_like_error(error):
163
- return False
164
- return backend_code_int(error) in RECORD_PERMISSION_DENIED_CODES
165
-
166
-
167
- def _is_optional_record_auxiliary_lookup_error(error: QingflowApiError) -> bool:
168
- if is_auth_like_error(error):
169
- return False
170
- return backend_code_int(error) in {40002, 40027, 404} or error.http_status == 404
171
134
 
172
135
 
173
136
  @dataclass(slots=True)
@@ -252,13 +215,6 @@ class AccessibleViewRoute:
252
215
  view_type: str | None = None
253
216
 
254
217
 
255
- def _prefer_custom_update_routes(routes: list[AccessibleViewRoute]) -> list[AccessibleViewRoute]:
256
- return [
257
- *[route for route in routes if route.kind == "custom"],
258
- *[route for route in routes if route.kind != "custom"],
259
- ]
260
-
261
-
262
218
  @dataclass(slots=True)
263
219
  class RecordContextRouteProbe:
264
220
  route: AccessibleViewRoute
@@ -366,12 +322,11 @@ class RecordTools(ToolBase):
366
322
  """注册当前工具到 MCP 服务。"""
367
323
  @mcp.tool()
368
324
  def record_insert_schema_get(
369
- profile: str = DEFAULT_PROFILE,
370
325
  app_key: str = "",
371
326
  output_profile: str = "normal",
372
327
  ) -> JSONObject:
373
328
  return self.record_insert_schema_get_public(
374
- profile=profile,
329
+ profile=DEFAULT_PROFILE,
375
330
  app_key=app_key,
376
331
  output_profile=output_profile,
377
332
  )
@@ -454,7 +409,6 @@ class RecordTools(ToolBase):
454
409
  where: list[JSONObject] | None = None,
455
410
  order_by: list[JSONObject] | None = None,
456
411
  page: int = 1,
457
- page_size: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
458
412
  view_id: str | None = None,
459
413
  output_profile: str = "normal",
460
414
  ) -> JSONObject:
@@ -467,7 +421,6 @@ class RecordTools(ToolBase):
467
421
  where=where or [],
468
422
  order_by=order_by or [],
469
423
  page=page,
470
- page_size=page_size,
471
424
  view_id=view_id,
472
425
  list_type=None,
473
426
  view_key=None,
@@ -483,7 +436,6 @@ class RecordTools(ToolBase):
483
436
  )
484
437
  )
485
438
  def record_access(
486
- profile: str = DEFAULT_PROFILE,
487
439
  app_key: str = "",
488
440
  view_id: str = "",
489
441
  columns: list[JSONObject | int] | None = None,
@@ -491,7 +443,7 @@ class RecordTools(ToolBase):
491
443
  order_by: list[JSONObject] | None = None,
492
444
  ) -> JSONObject:
493
445
  return self.record_access(
494
- profile=profile,
446
+ profile=DEFAULT_PROFILE,
495
447
  app_key=app_key,
496
448
  view_id=view_id,
497
449
  columns=columns or [],
@@ -506,6 +458,7 @@ class RecordTools(ToolBase):
506
458
  record_id: str = "",
507
459
  columns: list[JSONObject | int] | None = None,
508
460
  view_id: str | None = None,
461
+ workflow_node_id: int | None = None,
509
462
  output_profile: str = "detail_context",
510
463
  ) -> JSONObject:
511
464
  return self.record_get_public(
@@ -514,11 +467,11 @@ class RecordTools(ToolBase):
514
467
  record_id=record_id,
515
468
  columns=columns or [],
516
469
  view_id=view_id,
517
- workflow_node_id=None,
470
+ workflow_node_id=workflow_node_id,
518
471
  output_profile=output_profile,
519
472
  )
520
473
 
521
- @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.")
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.")
522
475
  def record_logs_get(
523
476
  profile: str = DEFAULT_PROFILE,
524
477
  app_key: str = "",
@@ -534,13 +487,12 @@ class RecordTools(ToolBase):
534
487
 
535
488
  @mcp.tool()
536
489
  def record_browse_schema_get(
537
- profile: str = DEFAULT_PROFILE,
538
490
  app_key: str = "",
539
491
  view_id: str = "",
540
492
  output_profile: str = "normal",
541
493
  ) -> JSONObject:
542
494
  return self.record_browse_schema_get_public(
543
- profile=profile,
495
+ profile=DEFAULT_PROFILE,
544
496
  app_key=app_key,
545
497
  view_id=view_id,
546
498
  output_profile=output_profile,
@@ -548,17 +500,14 @@ class RecordTools(ToolBase):
548
500
 
549
501
  @mcp.tool()
550
502
  def record_update_schema_get(
551
- profile: str = DEFAULT_PROFILE,
552
503
  app_key: str = "",
553
504
  record_id: str = "",
554
- view_id: str | None = None,
555
505
  output_profile: str = "normal",
556
506
  ) -> JSONObject:
557
507
  return self.record_update_schema_get_public(
558
- profile=profile,
508
+ profile=DEFAULT_PROFILE,
559
509
  app_key=app_key,
560
510
  record_id=record_id,
561
- view_id=view_id,
562
511
  output_profile=output_profile,
563
512
  )
564
513
 
@@ -571,14 +520,13 @@ class RecordTools(ToolBase):
571
520
  )
572
521
  )
573
522
  def record_insert(
574
- profile: str = DEFAULT_PROFILE,
575
523
  app_key: str = "",
576
524
  items: list[JSONObject] | None = None,
577
525
  verify_write: bool = True,
578
526
  output_profile: str = "normal",
579
527
  ) -> JSONObject:
580
528
  return self.record_insert_public(
581
- profile=profile,
529
+ profile=DEFAULT_PROFILE,
582
530
  app_key=app_key,
583
531
  items=items,
584
532
  verify_write=verify_write,
@@ -589,29 +537,25 @@ class RecordTools(ToolBase):
589
537
  description=(
590
538
  "Update one Qingflow record using a field map. "
591
539
  "For simple field changes, call this tool directly after the target record is clear. "
592
- "Pass view_id when the frontend detail view is known; the tool will try that view first. "
593
540
  "It first tries the data-manager direct route, then falls back to the frontend custom-view edit route when available. "
594
541
  "Use record_update_schema_get for diagnostics or complex field-scope inspection."
595
542
  )
596
543
  )
597
544
  def record_update(
598
- profile: str = DEFAULT_PROFILE,
599
545
  app_key: str = "",
600
546
  record_id: str | None = None,
601
547
  fields: JSONObject | None = None,
602
548
  items: list[JSONObject] | None = None,
603
- view_id: str | None = None,
604
549
  dry_run: bool = False,
605
550
  verify_write: bool = True,
606
551
  output_profile: str = "normal",
607
552
  ) -> JSONObject:
608
553
  return self.record_update_public(
609
- profile=profile,
554
+ profile=DEFAULT_PROFILE,
610
555
  app_key=app_key,
611
556
  record_id=record_id,
612
557
  fields=fields,
613
558
  items=items,
614
- view_id=view_id,
615
559
  dry_run=dry_run,
616
560
  verify_write=verify_write,
617
561
  output_profile=output_profile,
@@ -620,23 +564,20 @@ class RecordTools(ToolBase):
620
564
  @mcp.tool(
621
565
  description=(
622
566
  "Delete Qingflow records by record_id or record_ids. "
623
- "Pass view_id when deleting from a known system list route; custom view deletion is not supported by this backend route."
567
+ "This tool does not accept view selectors; resolve target record ids first, then delete by id."
624
568
  )
625
569
  )
626
570
  def record_delete(
627
- profile: str = DEFAULT_PROFILE,
628
571
  app_key: str = "",
629
572
  record_id: str | None = None,
630
573
  record_ids: list[str] | None = None,
631
- view_id: str | None = None,
632
574
  output_profile: str = "normal",
633
575
  ) -> JSONObject:
634
576
  return self.record_delete_public(
635
- profile=profile,
577
+ profile=DEFAULT_PROFILE,
636
578
  app_key=app_key,
637
579
  record_id=record_id,
638
580
  record_ids=record_ids or [],
639
- view_id=view_id,
640
581
  output_profile=output_profile,
641
582
  )
642
583
 
@@ -908,7 +849,6 @@ class RecordTools(ToolBase):
908
849
  profile: str = DEFAULT_PROFILE,
909
850
  app_key: str,
910
851
  record_id: Any,
911
- view_id: str | None = None,
912
852
  output_profile: str = "normal",
913
853
  ) -> JSONObject:
914
854
  """执行记录相关逻辑。"""
@@ -920,44 +860,21 @@ class RecordTools(ToolBase):
920
860
  def runner(session_profile, context):
921
861
  request_route = self._request_route_payload(context)
922
862
  self._clear_record_schema_caches(profile=profile, app_key=app_key, clear_view_caches=True)
923
- linkage_payloads_by_field_id: dict[str, JSONObject] = {}
924
- try:
925
- app_schema = self._get_form_schema(profile, context, app_key, force_refresh=True)
926
- except QingflowApiError as exc:
927
- if not _is_optional_schema_permission_error(exc):
928
- raise
929
- else:
930
- question_relations = _collect_question_relations(app_schema)
931
- app_index = _build_applicant_top_level_field_index(app_schema)
932
- linked_field_ids = _collect_linked_required_field_ids(question_relations)
933
- linked_field_ids.update(_collect_option_linked_field_ids(app_index))
934
- linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
935
- app_schema,
936
- linked_field_ids=linked_field_ids,
937
- )
938
- app_index = _merge_field_indexes(app_index, linked_hidden_index)
939
- linkage_payloads_by_field_id = _build_static_schema_linkage_payloads(
940
- index=app_index,
941
- question_relations=question_relations,
942
- )
943
- preferred_view_id = _normalize_optional_text(view_id)
863
+ app_schema = self._get_form_schema(profile, context, app_key, force_refresh=True)
864
+ question_relations = _collect_question_relations(app_schema)
865
+ app_index = _build_applicant_top_level_field_index(app_schema)
866
+ linked_field_ids = _collect_linked_required_field_ids(question_relations)
867
+ linked_field_ids.update(_collect_option_linked_field_ids(app_index))
868
+ linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
869
+ app_schema,
870
+ linked_field_ids=linked_field_ids,
871
+ )
872
+ app_index = _merge_field_indexes(app_index, linked_hidden_index)
873
+ linkage_payloads_by_field_id = _build_static_schema_linkage_payloads(
874
+ index=app_index,
875
+ question_relations=question_relations,
876
+ )
944
877
  candidate_routes = self._candidate_update_views(profile, context, app_key)
945
- if preferred_view_id:
946
- preferred_route = next(
947
- (
948
- route
949
- for route in candidate_routes
950
- if route.view_id == preferred_view_id
951
- ),
952
- None,
953
- )
954
- if preferred_route is None:
955
- raise_tool_error(
956
- QingflowApiError.config_error(
957
- f"view_id '{preferred_view_id}' is not an accessible update candidate"
958
- )
959
- )
960
- candidate_routes = [preferred_route]
961
878
  probes = self._probe_candidate_record_contexts(
962
879
  context,
963
880
  app_key=app_key,
@@ -1059,7 +976,6 @@ class RecordTools(ToolBase):
1059
976
  output_profile=normalized_output_profile,
1060
977
  view_probe_summary=probe_summary,
1061
978
  ambiguous_fields=[],
1062
- preferred_view_id=preferred_view_id,
1063
979
  )
1064
980
 
1065
981
  ambiguous_field_ids: set[int] = set()
@@ -1106,7 +1022,6 @@ class RecordTools(ToolBase):
1106
1022
  output_profile=normalized_output_profile,
1107
1023
  view_probe_summary=probe_summary,
1108
1024
  ambiguous_fields=ambiguous_fields,
1109
- preferred_view_id=preferred_view_id,
1110
1025
  )
1111
1026
 
1112
1027
  response: JSONObject = {
@@ -1131,8 +1046,6 @@ class RecordTools(ToolBase):
1131
1046
  "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.",
1132
1047
  },
1133
1048
  }
1134
- if preferred_view_id:
1135
- response["preferred_view_id"] = preferred_view_id
1136
1049
  if normalized_output_profile == "verbose":
1137
1050
  response["view_probe_summary"] = probe_summary
1138
1051
  response["record_context_probe"] = probe_summary
@@ -1193,7 +1106,6 @@ class RecordTools(ToolBase):
1193
1106
  output_profile: str,
1194
1107
  view_probe_summary: list[JSONObject],
1195
1108
  ambiguous_fields: list[JSONObject],
1196
- preferred_view_id: str | None = None,
1197
1109
  ) -> JSONObject:
1198
1110
  """执行内部辅助逻辑。"""
1199
1111
  response: JSONObject = {
@@ -1211,8 +1123,6 @@ class RecordTools(ToolBase):
1211
1123
  "payload_template": {},
1212
1124
  "recommended_next_actions": recommended_next_actions,
1213
1125
  }
1214
- if preferred_view_id:
1215
- response["preferred_view_id"] = preferred_view_id
1216
1126
  if output_profile == "verbose":
1217
1127
  response["view_probe_summary"] = view_probe_summary
1218
1128
  response["ambiguous_fields"] = ambiguous_fields
@@ -1530,58 +1440,24 @@ class RecordTools(ToolBase):
1530
1440
  )
1531
1441
  warnings: list[JSONObject] = []
1532
1442
  scope_source = "static_applicant_scope"
1533
- try:
1534
- if runtime_lookup:
1535
- state = self._build_candidate_lookup_state(
1536
- profile,
1537
- context,
1538
- app_key=app_key,
1539
- record_id=record_id_int,
1540
- workflow_node_id=workflow_node_id,
1541
- fields=normalized_fields,
1542
- )
1543
- items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
1544
- scope_source = "backend_runtime_scope"
1545
- else:
1546
- items: list[JSONObject] | None = None
1547
- if self._member_candidate_static_preview_should_use_backend(field):
1548
- state = self._build_candidate_lookup_state(
1549
- profile,
1550
- context,
1551
- app_key=app_key,
1552
- record_id=None,
1553
- workflow_node_id=None,
1554
- fields={},
1555
- )
1556
- items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
1557
- scope_source = "backend_applicant_scope"
1558
- if items is None:
1559
- items = self._resolve_member_candidates(context, field, keyword=keyword)
1560
- warnings.append(
1561
- {
1562
- "code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
1563
- "message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
1564
- }
1565
- )
1566
- except (RecordInputError, QingflowApiError) as error:
1567
- record_error = (
1568
- error
1569
- if isinstance(error, RecordInputError)
1570
- else self._candidate_lookup_error(kind="member", field=field, value=keyword, error=error)
1571
- )
1572
- return self._candidate_lookup_failed_response(
1573
- profile=profile,
1574
- session_profile=session_profile,
1575
- context=context,
1576
- kind="member",
1577
- error=record_error,
1578
- field=field,
1443
+ if runtime_lookup:
1444
+ state = self._build_candidate_lookup_state(
1445
+ profile,
1446
+ context,
1579
1447
  app_key=app_key,
1580
- record_id_text=record_id_text,
1448
+ record_id=record_id_int,
1581
1449
  workflow_node_id=workflow_node_id,
1582
- fields_present=bool(normalized_fields),
1583
- keyword=keyword,
1584
- scope_source=scope_source,
1450
+ fields=normalized_fields,
1451
+ )
1452
+ items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
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
+ }
1585
1461
  )
1586
1462
  total = len(items)
1587
1463
  start = (page_num - 1) * page_size
@@ -1674,75 +1550,41 @@ class RecordTools(ToolBase):
1674
1550
  )
1675
1551
  warnings: list[JSONObject] = []
1676
1552
  scope_source = "static_applicant_scope"
1677
- try:
1678
- if runtime_lookup:
1553
+ if runtime_lookup:
1554
+ state = self._build_candidate_lookup_state(
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
+ ):
1679
1573
  state = self._build_candidate_lookup_state(
1680
1574
  profile,
1681
1575
  context,
1682
1576
  app_key=app_key,
1683
- record_id=record_id_int,
1684
- workflow_node_id=workflow_node_id,
1685
- fields=normalized_fields,
1577
+ record_id=None,
1578
+ workflow_node_id=None,
1579
+ fields={},
1686
1580
  )
1687
1581
  items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
1688
1582
  scope_source = "backend_runtime_scope"
1689
- else:
1690
- items: list[JSONObject] | None = None
1691
- if self._department_candidate_static_preview_should_use_backend(field):
1692
- state = self._build_candidate_lookup_state(
1693
- profile,
1694
- context,
1695
- app_key=app_key,
1696
- record_id=None,
1697
- workflow_node_id=None,
1698
- fields={},
1699
- )
1700
- items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
1701
- scope_source = "backend_applicant_scope"
1702
- if items is None:
1703
- items = self._resolve_department_candidates(context, field, keyword=keyword)
1704
- scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
1705
- if (
1706
- not items
1707
- and field.dept_select_scope_type == 2
1708
- and not _scope_has_dynamic_or_external(scope)
1709
- and not list(scope.get("depart") or [])
1710
- ):
1711
- state = self._build_candidate_lookup_state(
1712
- profile,
1713
- context,
1714
- app_key=app_key,
1715
- record_id=None,
1716
- workflow_node_id=None,
1717
- fields={},
1718
- )
1719
- items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
1720
- scope_source = "backend_applicant_scope"
1721
- warnings.append(
1722
- {
1723
- "code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
1724
- "message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
1725
- }
1726
- )
1727
- except (RecordInputError, QingflowApiError) as error:
1728
- record_error = (
1729
- error
1730
- if isinstance(error, RecordInputError)
1731
- else self._candidate_lookup_error(kind="department", field=field, value=keyword, error=error)
1732
- )
1733
- return self._candidate_lookup_failed_response(
1734
- profile=profile,
1735
- session_profile=session_profile,
1736
- context=context,
1737
- kind="department",
1738
- error=record_error,
1739
- field=field,
1740
- app_key=app_key,
1741
- record_id_text=record_id_text,
1742
- workflow_node_id=workflow_node_id,
1743
- fields_present=bool(normalized_fields),
1744
- keyword=keyword,
1745
- scope_source=scope_source,
1583
+ warnings.append(
1584
+ {
1585
+ "code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
1586
+ "message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
1587
+ }
1746
1588
  )
1747
1589
  total = len(items)
1748
1590
  start = (page_num - 1) * page_size
@@ -1804,21 +1646,6 @@ class RecordTools(ToolBase):
1804
1646
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
1805
1647
  if limit <= 0:
1806
1648
  raise_tool_error(QingflowApiError.config_error("limit must be positive"))
1807
- if not (
1808
- _normalize_optional_text(view_id)
1809
- or list_type is not None
1810
- or _normalize_optional_text(view_key)
1811
- or _normalize_optional_text(view_name)
1812
- ):
1813
- raise_tool_error(
1814
- QingflowApiError.config_error(
1815
- "record_analyze requires view_id. Call app_get first and pass accessible_views[].view_id.",
1816
- details={
1817
- "error_code": "RECORD_ANALYZE_VIEW_REQUIRED",
1818
- "fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_analyze with view_id.",
1819
- },
1820
- )
1821
- )
1822
1649
  legacy_warnings = _detect_analyze_legacy_warnings(
1823
1650
  dimensions=dimensions,
1824
1651
  metrics=metrics,
@@ -1835,7 +1662,7 @@ class RecordTools(ToolBase):
1835
1662
  list_type=list_type,
1836
1663
  view_key=view_key,
1837
1664
  view_name=view_name,
1838
- allow_default=False,
1665
+ allow_default=True,
1839
1666
  )
1840
1667
  if not _view_type_supports_analysis(resolved_view.view_type):
1841
1668
  raise_tool_error(
@@ -1898,7 +1725,6 @@ class RecordTools(ToolBase):
1898
1725
  order_by: list[JSONObject],
1899
1726
  limit: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
1900
1727
  page: int = 1,
1901
- page_size: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
1902
1728
  view_id: str | None = None,
1903
1729
  list_type: int | None = None,
1904
1730
  view_key: str | None = None,
@@ -1917,23 +1743,6 @@ class RecordTools(ToolBase):
1917
1743
  raise_tool_error(QingflowApiError.config_error("limit must be positive"))
1918
1744
  if page <= 0:
1919
1745
  raise_tool_error(QingflowApiError.config_error("page must be positive"))
1920
- if page_size <= 0:
1921
- raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
1922
- if not (
1923
- _normalize_optional_text(view_id)
1924
- or list_type is not None
1925
- or _normalize_optional_text(view_key)
1926
- or _normalize_optional_text(view_name)
1927
- ):
1928
- raise_tool_error(
1929
- QingflowApiError.config_error(
1930
- "record_list requires view_id. Call app_get first and pass accessible_views[].view_id.",
1931
- details={
1932
- "error_code": "RECORD_LIST_VIEW_REQUIRED",
1933
- "fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_list with view_id.",
1934
- },
1935
- )
1936
- )
1937
1746
  view_route, compatibility_warnings = self._resolve_accessible_view_route_for_public(
1938
1747
  profile=profile,
1939
1748
  app_key=app_key,
@@ -1941,7 +1750,7 @@ class RecordTools(ToolBase):
1941
1750
  list_type=list_type,
1942
1751
  view_key=view_key,
1943
1752
  view_name=view_name,
1944
- allow_default=False,
1753
+ allow_default=True,
1945
1754
  )
1946
1755
  if not _view_type_supports_analysis(view_route.view_type):
1947
1756
  raise_tool_error(
@@ -1991,12 +1800,12 @@ class RecordTools(ToolBase):
1991
1800
  app_key=app_key,
1992
1801
  view_route=view_route,
1993
1802
  page_num=page,
1994
- page_size=page_size,
1803
+ page_size=DEFAULT_LIST_PAGE_SIZE,
1995
1804
  query_key=normalized_query,
1996
1805
  search_que_ids=resolved_query_fields or None,
1997
1806
  match_rules=match_rules,
1998
1807
  sort_rules=sort_rules,
1999
- max_rows=min(limit, page_size),
1808
+ max_rows=limit,
2000
1809
  selected_fields=selected_fields,
2001
1810
  output_profile="verbose" if normalized_output_profile in {"verbose", "normalized"} else DEFAULT_OUTPUT_PROFILE,
2002
1811
  )
@@ -2415,7 +2224,6 @@ class RecordTools(ToolBase):
2415
2224
  requested_output_profile = _normalize_record_get_detail_output_profile(output_profile)
2416
2225
  record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
2417
2226
  normalized_columns = _normalize_public_column_selectors(columns)
2418
- explicit_view_id = _normalize_optional_text(view_id)
2419
2227
 
2420
2228
  def runner(session_profile, context):
2421
2229
  resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
@@ -2441,45 +2249,17 @@ class RecordTools(ToolBase):
2441
2249
  "code": "OUTPUT_PROFILE_DEPRECATED_FOR_DETAIL_CONTEXT",
2442
2250
  "message": f"output_profile={requested_output_profile!r} is deprecated for record_get; detail_context is always returned.",
2443
2251
  })
2444
- def get_detail_for_route(route: AccessibleViewRoute, route_warnings: list[JSONObject]) -> JSONObject:
2445
- return self._record_get_detail_context(
2446
- profile=profile,
2447
- session_profile=session_profile,
2448
- context=context,
2449
- app_key=app_key,
2450
- record_id_int=record_id_int,
2451
- resolved_view=route,
2452
- requested_focus_field_ids=normalized_columns,
2453
- workflow_node_id=workflow_node_id,
2454
- warnings=route_warnings,
2455
- )
2456
-
2457
- try:
2458
- return get_detail_for_route(resolved_view, warnings)
2459
- except QingflowApiError as exc:
2460
- if explicit_view_id is not None:
2461
- raise
2462
- if not self._is_record_context_route_miss(exc):
2463
- raise
2464
- fallback_warnings = list(warnings)
2465
- fallback_warnings.append(
2466
- {
2467
- "code": "DEFAULT_DETAIL_ROUTE_DENIED",
2468
- "message": "record_get default system:all route was not readable; trying accessible views that match the frontend route model.",
2469
- "backend_code": exc.backend_code,
2470
- }
2471
- )
2472
- last_error = exc
2473
- for candidate in self._candidate_update_views(profile, context, app_key):
2474
- if candidate.view_id == resolved_view.view_id:
2475
- continue
2476
- try:
2477
- return get_detail_for_route(candidate, fallback_warnings)
2478
- except QingflowApiError as candidate_exc:
2479
- if not self._is_record_context_route_miss(candidate_exc):
2480
- raise
2481
- last_error = candidate_exc
2482
- raise last_error
2252
+ return self._record_get_detail_context(
2253
+ profile=profile,
2254
+ session_profile=session_profile,
2255
+ context=context,
2256
+ app_key=app_key,
2257
+ record_id_int=record_id_int,
2258
+ resolved_view=resolved_view,
2259
+ requested_focus_field_ids=normalized_columns,
2260
+ workflow_node_id=workflow_node_id,
2261
+ warnings=warnings,
2262
+ )
2483
2263
 
2484
2264
  return self._run_record_tool(profile, runner)
2485
2265
 
@@ -2494,16 +2274,6 @@ class RecordTools(ToolBase):
2494
2274
  ) -> JSONObject:
2495
2275
  """读取单条记录可见的全量数据日志和流程日志,写入本地 JSONL。"""
2496
2276
  record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
2497
- if not _normalize_optional_text(view_id):
2498
- raise_tool_error(
2499
- QingflowApiError.config_error(
2500
- "record_logs_get requires view_id. Call app_get first and pass accessible_views[].view_id.",
2501
- details={
2502
- "error_code": "RECORD_LOGS_VIEW_REQUIRED",
2503
- "fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_logs_get with view_id.",
2504
- },
2505
- )
2506
- )
2507
2277
 
2508
2278
  def runner(session_profile, context):
2509
2279
  resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
@@ -2514,45 +2284,21 @@ class RecordTools(ToolBase):
2514
2284
  list_type=None,
2515
2285
  view_key=None,
2516
2286
  view_name=None,
2517
- allow_default=False,
2287
+ allow_default=True,
2518
2288
  )
2519
2289
  warnings: list[JSONObject] = []
2520
2290
  warnings.extend(compatibility_warnings)
2521
2291
  warnings.extend(_view_filter_trust_warnings(resolved_view))
2522
2292
  unavailable_context: list[JSONObject] = []
2523
2293
 
2524
- schema: JSONObject = {}
2525
- try:
2526
- schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
2527
- except QingflowApiError as exc:
2528
- if not _is_optional_schema_permission_error(exc):
2529
- raise
2530
- unavailable_context.append(
2531
- _record_detail_unavailable_context(
2532
- "detail_schema",
2533
- "记录日志字段结构辅助信息获取失败,已尝试使用详情主数据中的字段信息继续。",
2534
- exc,
2535
- )
2536
- )
2294
+ schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
2537
2295
  index = _build_top_level_field_index(schema)
2538
- try:
2539
- audit_info = self._record_get_audit_info(
2540
- context,
2541
- app_key=app_key,
2542
- record_id=record_id_int,
2543
- resolved_view=resolved_view,
2544
- )
2545
- except QingflowApiError as exc:
2546
- if not _is_optional_schema_permission_error(exc):
2547
- raise
2548
- audit_info = {}
2549
- unavailable_context.append(
2550
- _record_detail_unavailable_context(
2551
- "audit_info",
2552
- "记录审批节点辅助信息获取失败,已继续读取详情主数据和日志。",
2553
- exc,
2554
- )
2555
- )
2296
+ audit_info = self._record_get_audit_info(
2297
+ context,
2298
+ app_key=app_key,
2299
+ record_id=record_id_int,
2300
+ resolved_view=resolved_view,
2301
+ )
2556
2302
  audit_context = _record_detail_audit_context(audit_info, workflow_node_id=None)
2557
2303
  detail_result, used_list_type, used_role = self._record_get_apply_detail(
2558
2304
  context,
@@ -2562,17 +2308,6 @@ class RecordTools(ToolBase):
2562
2308
  audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
2563
2309
  )
2564
2310
  answer_list = _record_detail_answers(detail_result)
2565
- if not index.by_id:
2566
- answer_index = _build_answer_backed_field_index(cast(list[JSONObject], answer_list))
2567
- if answer_index.by_id:
2568
- index = answer_index
2569
- unavailable_context.append(
2570
- {
2571
- "section": "detail_schema",
2572
- "message": "字段结构由详情 answers 回退构造;候选范围、只读状态、选项和联动信息可能不完整。",
2573
- "category": "partial_context",
2574
- }
2575
- )
2576
2311
  selected_fields = list(index.by_id.values())
2577
2312
  fields = [
2578
2313
  _record_detail_field_payload(field, _find_answer_for_field(cast(list[JSONValue], answer_list), field), focus_id_set=set())
@@ -2666,41 +2401,14 @@ class RecordTools(ToolBase):
2666
2401
  warnings: list[JSONObject],
2667
2402
  ) -> JSONObject:
2668
2403
  """执行内部辅助逻辑。"""
2669
- unavailable_context: list[JSONObject] = []
2670
- schema: JSONObject = {}
2671
- schema_available = True
2672
- try:
2673
- schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
2674
- except QingflowApiError as exc:
2675
- if not _is_optional_schema_permission_error(exc):
2676
- raise
2677
- schema_available = False
2678
- unavailable_context.append(
2679
- _record_detail_unavailable_context(
2680
- "detail_schema",
2681
- "记录详情字段结构辅助信息获取失败,已尝试使用详情主数据中的字段信息继续。",
2682
- exc,
2683
- )
2684
- )
2404
+ schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
2685
2405
  index = _build_top_level_field_index(schema)
2686
- try:
2687
- audit_info = self._record_get_audit_info(
2688
- context,
2689
- app_key=app_key,
2690
- record_id=record_id_int,
2691
- resolved_view=resolved_view,
2692
- )
2693
- except QingflowApiError as exc:
2694
- if not _is_optional_schema_permission_error(exc):
2695
- raise
2696
- audit_info = {}
2697
- unavailable_context.append(
2698
- _record_detail_unavailable_context(
2699
- "audit_info",
2700
- "记录审批节点辅助信息获取失败,已继续读取详情主数据。",
2701
- exc,
2702
- )
2703
- )
2406
+ audit_info = self._record_get_audit_info(
2407
+ context,
2408
+ app_key=app_key,
2409
+ record_id=record_id_int,
2410
+ resolved_view=resolved_view,
2411
+ )
2704
2412
  audit_context = _record_detail_audit_context(audit_info, workflow_node_id=workflow_node_id)
2705
2413
  detail_result, used_list_type, used_role = self._record_get_apply_detail(
2706
2414
  context,
@@ -2710,24 +2418,13 @@ class RecordTools(ToolBase):
2710
2418
  audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
2711
2419
  )
2712
2420
  answer_list = _record_detail_answers(detail_result)
2713
- if not index.by_id:
2714
- answer_index = _build_answer_backed_field_index(cast(list[JSONObject], answer_list))
2715
- if answer_index.by_id:
2716
- index = answer_index
2717
- unavailable_context.append(
2718
- {
2719
- "section": "detail_schema",
2720
- "message": "字段结构由详情 answers 回退构造;候选范围、只读状态、选项和联动信息可能不完整。",
2721
- "category": "partial_context",
2722
- }
2723
- )
2724
2421
  selected_fields = list(index.by_id.values())
2725
2422
  row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id_int)
2726
2423
  normalized_record, _normalized_ambiguous_fields = _build_normalized_row_from_answers(
2727
2424
  cast(list[JSONValue], answer_list),
2728
2425
  selected_fields,
2729
2426
  )
2730
- if schema_available and self._record_get_needs_schema_refresh(
2427
+ if self._record_get_needs_schema_refresh(
2731
2428
  answer_list=cast(list[JSONValue], answer_list),
2732
2429
  selected_fields=selected_fields,
2733
2430
  record=row,
@@ -2743,6 +2440,7 @@ class RecordTools(ToolBase):
2743
2440
  index = _build_top_level_field_index(schema)
2744
2441
  selected_fields = list(index.by_id.values())
2745
2442
 
2443
+ unavailable_context: list[JSONObject] = []
2746
2444
  dynamic_reference_answers, dynamic_reference_unavailable = self._record_get_dynamic_reference_answers(
2747
2445
  context,
2748
2446
  app_key=app_key,
@@ -2901,20 +2599,7 @@ class RecordTools(ToolBase):
2901
2599
  ) -> JSONObject:
2902
2600
  """执行内部辅助逻辑。"""
2903
2601
  if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
2904
- return self._get_custom_view_browse_schema(
2905
- profile,
2906
- context,
2907
- resolved_view.view_selection.view_key,
2908
- force_refresh=force_refresh,
2909
- )
2910
- if resolved_view.kind == "system" and resolved_view.list_type is not None:
2911
- return self._get_system_browse_schema(
2912
- profile,
2913
- context,
2914
- app_key,
2915
- list_type=resolved_view.list_type,
2916
- force_refresh=force_refresh,
2917
- )
2602
+ return self._get_view_form_schema(profile, context, resolved_view.view_selection.view_key, force_refresh=force_refresh)
2918
2603
  return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
2919
2604
 
2920
2605
  def _record_get_audit_info(
@@ -2975,7 +2660,7 @@ class RecordTools(ToolBase):
2975
2660
  )
2976
2661
  return result if isinstance(result, dict) else {"value": result}, list_type, role
2977
2662
  except QingflowApiError as exc:
2978
- if resolved_view.list_type is not None or not _is_record_permission_denied_error(exc):
2663
+ if resolved_view.list_type is not None or exc.backend_code != 40002:
2979
2664
  raise
2980
2665
  last_error: QingflowApiError = exc
2981
2666
  for fallback_list_type in (14, 1, 2, 12):
@@ -2993,7 +2678,7 @@ class RecordTools(ToolBase):
2993
2678
  return result if isinstance(result, dict) else {"value": result}, fallback_list_type, role
2994
2679
  except QingflowApiError as fallback_exc:
2995
2680
  last_error = fallback_exc
2996
- if _is_record_permission_denied_error(fallback_exc):
2681
+ if fallback_exc.backend_code == 40002:
2997
2682
  continue
2998
2683
  raise
2999
2684
  raise last_error
@@ -3091,8 +2776,6 @@ class RecordTools(ToolBase):
3091
2776
  if target_app_key == app_key and str(target_record_id) == str(source_record_id):
3092
2777
  reference_payload["self_reference"] = True
3093
2778
  except QingflowApiError as exc:
3094
- if is_auth_like_error(exc):
3095
- raise
3096
2779
  unavailable = _record_detail_unavailable_context(
3097
2780
  "reference_detail",
3098
2781
  f"引用字段「{field.que_title}」的目标记录详情获取失败。",
@@ -3190,8 +2873,6 @@ class RecordTools(ToolBase):
3190
2873
  json_body=body,
3191
2874
  )
3192
2875
  except QingflowApiError as exc:
3193
- if is_auth_like_error(exc):
3194
- raise
3195
2876
  unavailable = _record_detail_unavailable_context(
3196
2877
  "reference_runtime_match",
3197
2878
  "动态引用字段匹配数据获取失败。",
@@ -3246,8 +2927,6 @@ class RecordTools(ToolBase):
3246
2927
  },
3247
2928
  )
3248
2929
  except QingflowApiError as exc:
3249
- if is_auth_like_error(exc):
3250
- raise
3251
2930
  unavailable_context.append(_record_detail_unavailable_context("log_visibility", "侧边栏日志可见性获取失败。", exc))
3252
2931
  return {"status": "unavailable", "channel": channel, "data_log_visible": None, "workflow_log_visible": None}
3253
2932
  payload = visibility if isinstance(visibility, dict) else {}
@@ -3301,8 +2980,6 @@ class RecordTools(ToolBase):
3301
2980
  source="data_logs",
3302
2981
  )
3303
2982
  except QingflowApiError as exc:
3304
- if is_auth_like_error(exc):
3305
- raise
3306
2983
  unavailable_context.append(_record_detail_unavailable_context("data_logs", "最近数据日志获取失败。", exc))
3307
2984
  return _record_detail_log_unavailable_payload("data_logs", "fetch_unavailable")
3308
2985
 
@@ -3356,8 +3033,6 @@ class RecordTools(ToolBase):
3356
3033
  source="workflow_logs",
3357
3034
  )
3358
3035
  except QingflowApiError as exc:
3359
- if is_auth_like_error(exc):
3360
- raise
3361
3036
  unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "流程日志本次获取失败。", exc))
3362
3037
  return _record_detail_log_unavailable_payload("workflow_logs", "fetch_unavailable")
3363
3038
 
@@ -3401,8 +3076,6 @@ class RecordTools(ToolBase):
3401
3076
  deadline=deadline,
3402
3077
  )
3403
3078
  except QingflowApiError as exc:
3404
- if is_auth_like_error(exc):
3405
- raise
3406
3079
  unavailable_context.append(_record_detail_unavailable_context("data_logs", "全量数据日志获取失败。", exc))
3407
3080
  return _record_logs_unavailable_payload("data_logs", "fetch_unavailable")
3408
3081
 
@@ -3462,8 +3135,6 @@ class RecordTools(ToolBase):
3462
3135
  deadline=deadline,
3463
3136
  )
3464
3137
  except QingflowApiError as exc:
3465
- if is_auth_like_error(exc):
3466
- raise
3467
3138
  unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "全量流程日志获取失败。", exc))
3468
3139
  return _record_logs_unavailable_payload("workflow_logs", "fetch_unavailable")
3469
3140
 
@@ -3496,8 +3167,6 @@ class RecordTools(ToolBase):
3496
3167
  params["auditNodeId"] = audit_node_id
3497
3168
  payload = self.backend.request("GET", context, f"/app/{app_key}/asosChart", params=params)
3498
3169
  except QingflowApiError as exc:
3499
- if is_auth_like_error(exc):
3500
- raise
3501
3170
  unavailable_context.append(_record_detail_unavailable_context("associated_resources", "关联资源获取失败。", exc))
3502
3171
  return []
3503
3172
  return [_record_detail_associated_resource(item) for item in _record_detail_associated_resource_items(payload)]
@@ -3535,17 +3204,16 @@ class RecordTools(ToolBase):
3535
3204
  refresh_source_url=refresh_source_url,
3536
3205
  )
3537
3206
  except Exception as exc: # defensive: media should never break the core record detail.
3538
- warning: JSONObject = {
3539
- "code": "MEDIA_ASSETS_UNAVAILABLE",
3540
- "message": f"record_get could not collect media assets: {exc}",
3541
- }
3542
- if isinstance(exc, QingflowApiError):
3543
- warning.update(_record_detail_error_warning_fields(exc))
3544
3207
  return {
3545
3208
  "status": "unavailable",
3546
3209
  "local_dir": None,
3547
3210
  "items": [],
3548
- "warnings": [warning],
3211
+ "warnings": [
3212
+ {
3213
+ "code": "MEDIA_ASSETS_UNAVAILABLE",
3214
+ "message": f"record_get could not collect media assets: {exc}",
3215
+ }
3216
+ ],
3549
3217
  }
3550
3218
 
3551
3219
  def _record_get_file_assets(
@@ -3583,17 +3251,16 @@ class RecordTools(ToolBase):
3583
3251
  refresh_source_url=refresh_source_url,
3584
3252
  )
3585
3253
  except Exception as exc: # defensive: file assets should never break the core record detail.
3586
- warning = {
3587
- "code": "FILE_ASSETS_UNAVAILABLE",
3588
- "message": f"record_get could not collect file assets: {exc}",
3589
- }
3590
- if isinstance(exc, QingflowApiError):
3591
- warning.update(_record_detail_error_warning_fields(exc))
3592
3254
  return {
3593
3255
  "status": "unavailable",
3594
3256
  "local_dir": None,
3595
3257
  "items": [],
3596
- "warnings": [warning],
3258
+ "warnings": [
3259
+ {
3260
+ "code": "FILE_ASSETS_UNAVAILABLE",
3261
+ "message": f"record_get could not collect file assets: {exc}",
3262
+ }
3263
+ ],
3597
3264
  }
3598
3265
 
3599
3266
  def _record_get_refreshed_media_source_url(
@@ -3605,7 +3272,7 @@ class RecordTools(ToolBase):
3605
3272
  resolved_view: AccessibleViewRoute,
3606
3273
  audit_node_id: int | None,
3607
3274
  candidate: JSONObject,
3608
- ) -> JSONValue | None:
3275
+ ) -> str | None:
3609
3276
  """Refresh the detail payload once to recover an expired attachment storage signature."""
3610
3277
  if candidate.get("source") not in {"attachment", "image_field", "subtable"}:
3611
3278
  return None
@@ -3621,15 +3288,8 @@ class RecordTools(ToolBase):
3621
3288
  resolved_view=resolved_view,
3622
3289
  audit_node_id=audit_node_id,
3623
3290
  )
3624
- except QingflowApiError as exc:
3625
- return {
3626
- "source_url": None,
3627
- "warning": _record_detail_unavailable_context(
3628
- "asset_url_refresh",
3629
- "record_get could not refresh the record detail before downloading a private asset.",
3630
- exc,
3631
- ),
3632
- }
3291
+ except QingflowApiError:
3292
+ return None
3633
3293
  for answer in _record_detail_answers(detail_result):
3634
3294
  if not isinstance(answer, dict) or _coerce_count(answer.get("queId")) != field_id:
3635
3295
  continue
@@ -3665,7 +3325,6 @@ class RecordTools(ToolBase):
3665
3325
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
3666
3326
  if items is not None:
3667
3327
  normalized_items = self._normalize_public_record_insert_batch_items(fields=fields, items=items)
3668
- self._reject_record_insert_system_fields([cast(JSONObject, item["fields"]) for item in normalized_items])
3669
3328
  return self._record_insert_public_batch(
3670
3329
  profile=profile,
3671
3330
  app_key=app_key,
@@ -3675,35 +3334,15 @@ class RecordTools(ToolBase):
3675
3334
  )
3676
3335
  if fields is not None and not isinstance(fields, dict):
3677
3336
  raise_tool_error(QingflowApiError.config_error("fields must be an object map keyed by field title"))
3678
- normalized_fields = cast(JSONObject, fields or {})
3679
- self._reject_record_insert_system_fields([normalized_fields])
3680
3337
  return self._record_insert_public_single(
3681
3338
  profile=profile,
3682
3339
  app_key=app_key,
3683
- fields=normalized_fields,
3340
+ fields=cast(JSONObject, fields or {}),
3684
3341
  verify_write=verify_write,
3685
3342
  output_profile=normalized_output_profile,
3686
3343
  capture_exceptions=False,
3687
3344
  )
3688
3345
 
3689
- def _reject_record_insert_system_fields(self, field_maps: list[JSONObject]) -> None:
3690
- for row_index, field_map in enumerate(field_maps):
3691
- for field_name in field_map:
3692
- normalized_name = str(field_name or "").strip()
3693
- if normalized_name in RECORD_WRITE_SYSTEM_FIELD_NAMES:
3694
- raise_tool_error(
3695
- QingflowApiError.config_error(
3696
- f"record_insert fields must not include built-in system field '{normalized_name}'",
3697
- details={
3698
- "error_code": "RESERVED_SYSTEM_FIELD_NAME",
3699
- "row_number": row_index + 1,
3700
- "field_name": normalized_name,
3701
- "reserved_field_names": sorted(RECORD_WRITE_SYSTEM_FIELD_NAMES),
3702
- "fix_hint": "Remove Qingflow built-in system fields from record_insert payload. They are generated by the platform and can be read after creation, not manually inserted.",
3703
- },
3704
- )
3705
- )
3706
-
3707
3346
  def _record_insert_public_single(
3708
3347
  self,
3709
3348
  *,
@@ -4037,11 +3676,7 @@ class RecordTools(ToolBase):
4037
3676
  "field_id": field_id,
4038
3677
  "error_code": error_code,
4039
3678
  "message": self._record_write_semantic_error_message(error_code, error.get("message")),
4040
- "next_action": self._record_write_next_action_for_error(
4041
- error_code,
4042
- expected_format=expected_format,
4043
- field_payload=field_payload,
4044
- ),
3679
+ "next_action": self._record_write_next_action_for_error(error_code),
4045
3680
  }
4046
3681
  if expected_format is not None:
4047
3682
  payload["expected_format"] = expected_format
@@ -4152,30 +3787,8 @@ class RecordTools(ToolBase):
4152
3787
  return _normalize_optional_text(fallback) or "字段值格式不正确。"
4153
3788
  return _normalize_optional_text(fallback) or "字段写入失败。"
4154
3789
 
4155
- def _record_write_next_action_for_error(
4156
- self,
4157
- error_code: str,
4158
- *,
4159
- expected_format: JSONObject | None = None,
4160
- field_payload: JSONObject | None = None,
4161
- ) -> str:
3790
+ def _record_write_next_action_for_error(self, error_code: str) -> str:
4162
3791
  """执行内部辅助逻辑。"""
4163
- kind = _normalize_optional_text((expected_format or {}).get("kind"))
4164
- field_title = (
4165
- _normalize_optional_text((field_payload or {}).get("que_title"))
4166
- or _normalize_optional_text((field_payload or {}).get("title"))
4167
- or "该字段"
4168
- )
4169
- if kind == "member_list":
4170
- return f"{field_title} 需要有效成员;用唯一姓名/邮箱/id,歧义时先调用 record_member_candidates 后只重试本行。"
4171
- if kind == "department_list":
4172
- return f"{field_title} 需要有效部门;用候选范围内的部门名/id/object,歧义或不确定时先调用 record_department_candidates 后只重试本行。"
4173
- if kind == "relation_record":
4174
- return f"{field_title} 需要关联记录;优先改用目标记录 record_id/apply_id,或使用可唯一命中的显示文本后只重试本行。"
4175
- if kind == "attachment_list":
4176
- return f"{field_title} 需要附件值;先用 file_upload_local 上传文件,再写返回的 value/url 后只重试本行。"
4177
- if kind in {"single_select", "multi_select"}:
4178
- return f"{field_title} 需要选项值;使用 schema options 中的标签或 option id,修正后只重试本行。"
4179
3792
  if error_code == "MISSING_REQUIRED_FIELD":
4180
3793
  return "补充该字段后只重试本行。"
4181
3794
  if error_code in {"FIELD_NOT_FOUND", "AMBIGUOUS_FIELD"}:
@@ -4191,7 +3804,6 @@ class RecordTools(ToolBase):
4191
3804
  record_id: Any | None,
4192
3805
  fields: JSONObject | None = None,
4193
3806
  items: list[JSONObject] | None = None,
4194
- view_id: str | None = None,
4195
3807
  dry_run: bool = False,
4196
3808
  verify_write: bool = True,
4197
3809
  output_profile: str = "normal",
@@ -4212,7 +3824,6 @@ class RecordTools(ToolBase):
4212
3824
  profile=profile,
4213
3825
  app_key=app_key,
4214
3826
  items=normalized_items,
4215
- view_id=view_id,
4216
3827
  dry_run=dry_run,
4217
3828
  verify_write=verify_write,
4218
3829
  output_profile=normalized_output_profile,
@@ -4229,7 +3840,6 @@ class RecordTools(ToolBase):
4229
3840
  app_key=app_key,
4230
3841
  record_id=record_id_int,
4231
3842
  fields=cast(JSONObject, fields or {}),
4232
- view_id=view_id,
4233
3843
  verify_write=verify_write,
4234
3844
  output_profile=normalized_output_profile,
4235
3845
  )
@@ -4241,61 +3851,17 @@ class RecordTools(ToolBase):
4241
3851
  app_key: str,
4242
3852
  record_id: int,
4243
3853
  fields: JSONObject,
4244
- view_id: str | None,
4245
3854
  verify_write: bool,
4246
3855
  output_profile: str,
4247
- capture_exceptions: bool = False,
4248
3856
  ) -> JSONObject:
4249
3857
  """执行内部辅助逻辑。"""
4250
- write_state = {"attempted": False}
4251
- try:
4252
- return self._record_update_public_single_impl(
4253
- profile=profile,
4254
- app_key=app_key,
4255
- record_id=record_id,
4256
- fields=fields,
4257
- view_id=view_id,
4258
- verify_write=verify_write,
4259
- output_profile=output_profile,
4260
- write_attempted_ref=lambda value: write_state.__setitem__("attempted", value),
4261
- )
4262
- except (QingflowApiError, RuntimeError) as exc:
4263
- if not capture_exceptions:
4264
- raise
4265
- return self._record_write_exception_response(
4266
- exc,
4267
- operation="update",
4268
- profile=profile,
4269
- app_key=app_key,
4270
- record_id=record_id,
4271
- output_profile=output_profile,
4272
- human_review=True,
4273
- write_executed=write_state["attempted"],
4274
- )
4275
-
4276
- def _record_update_public_single_impl(
4277
- self,
4278
- *,
4279
- profile: str,
4280
- app_key: str,
4281
- record_id: int,
4282
- fields: JSONObject,
4283
- view_id: str | None,
4284
- verify_write: bool,
4285
- output_profile: str,
4286
- write_attempted_ref: Callable[[bool], None],
4287
- ) -> JSONObject:
4288
- """执行内部辅助逻辑。"""
4289
- preflight_kwargs: dict[str, Any] = {
4290
- "profile": profile,
4291
- "app_key": app_key,
4292
- "record_id": record_id,
4293
- "fields": fields,
4294
- "force_refresh_form": False,
4295
- }
4296
- if view_id is not None:
4297
- preflight_kwargs["preferred_view_id"] = view_id
4298
- raw_preflight = self._preflight_record_update_with_auto_view(**preflight_kwargs)
3858
+ raw_preflight = self._preflight_record_update_with_auto_view(
3859
+ profile=profile,
3860
+ app_key=app_key,
3861
+ record_id=record_id,
3862
+ fields=fields,
3863
+ force_refresh_form=False,
3864
+ )
4299
3865
  preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
4300
3866
  preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
4301
3867
  normalized_payload = self._record_write_normalized_payload(
@@ -4315,7 +3881,6 @@ class RecordTools(ToolBase):
4315
3881
  human_review=True,
4316
3882
  target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
4317
3883
  )
4318
- write_attempted_ref(True)
4319
3884
  route_apply, tried_routes, route_blocker = self._record_update_apply_with_auto_route(
4320
3885
  profile=profile,
4321
3886
  app_key=app_key,
@@ -4596,9 +4161,7 @@ class RecordTools(ToolBase):
4596
4161
  )
4597
4162
 
4598
4163
  def _record_update_route_permission_denied(self, exc: QingflowApiError) -> bool:
4599
- if is_auth_like_error(exc):
4600
- return False
4601
- if backend_code_int(exc) in {40002, 40027, 40038, 404}:
4164
+ if exc.backend_code in {40002, 40027, 40038, 404}:
4602
4165
  return True
4603
4166
  if exc.http_status == 404:
4604
4167
  return True
@@ -4702,8 +4265,6 @@ class RecordTools(ToolBase):
4702
4265
  },
4703
4266
  )
4704
4267
  except QingflowApiError as exc:
4705
- if not _is_optional_record_auxiliary_lookup_error(exc):
4706
- raise
4707
4268
  return unavailable(
4708
4269
  error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
4709
4270
  reason="current-user todo task list is unavailable",
@@ -4751,8 +4312,6 @@ class RecordTools(ToolBase):
4751
4312
  f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
4752
4313
  )
4753
4314
  except QingflowApiError as exc:
4754
- if not _is_optional_record_auxiliary_lookup_error(exc):
4755
- raise
4756
4315
  return unavailable(
4757
4316
  error_code="TASK_EDITABLE_FIELDS_UNAVAILABLE",
4758
4317
  reason="workflow node editable field list is unavailable; record_update will not guess task editability",
@@ -4903,7 +4462,7 @@ class RecordTools(ToolBase):
4903
4462
  raise_tool_error(QingflowApiError.config_error("view_key is required for custom view update"))
4904
4463
 
4905
4464
  def runner(session_profile, context):
4906
- index = self._get_view_field_index(profile, context, normalized_view_key, force_refresh=force_refresh_form) if verify_write else None
4465
+ index = self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form) if verify_write else None
4907
4466
  normalized_answers = [dict(item) for item in answers if isinstance(item, dict)]
4908
4467
  self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
4909
4468
  result = self.backend.request(
@@ -5006,7 +4565,6 @@ class RecordTools(ToolBase):
5006
4565
  profile: str,
5007
4566
  app_key: str,
5008
4567
  items: list[JSONObject],
5009
- view_id: str | None,
5010
4568
  dry_run: bool,
5011
4569
  verify_write: bool,
5012
4570
  output_profile: str,
@@ -5018,7 +4576,6 @@ class RecordTools(ToolBase):
5018
4576
  app_key=app_key,
5019
4577
  record_id=cast(int, item["record_id"]),
5020
4578
  fields=cast(JSONObject, item["fields"]),
5021
- view_id=view_id,
5022
4579
  output_profile=output_profile,
5023
4580
  )
5024
4581
  for item in items
@@ -5047,10 +4604,8 @@ class RecordTools(ToolBase):
5047
4604
  app_key=app_key,
5048
4605
  record_id=record_id,
5049
4606
  fields=fields,
5050
- view_id=view_id,
5051
4607
  verify_write=verify_write,
5052
4608
  output_profile=output_profile,
5053
- capture_exceptions=True,
5054
4609
  )
5055
4610
  )
5056
4611
  except (QingflowApiError, RuntimeError) as exc:
@@ -5081,20 +4636,16 @@ class RecordTools(ToolBase):
5081
4636
  app_key: str,
5082
4637
  record_id: int,
5083
4638
  fields: JSONObject,
5084
- view_id: str | None,
5085
4639
  output_profile: str,
5086
4640
  ) -> JSONObject:
5087
4641
  """执行内部辅助逻辑。"""
5088
- preflight_kwargs: dict[str, Any] = {
5089
- "profile": profile,
5090
- "app_key": app_key,
5091
- "record_id": record_id,
5092
- "fields": fields,
5093
- "force_refresh_form": False,
5094
- }
5095
- if view_id is not None:
5096
- preflight_kwargs["preferred_view_id"] = view_id
5097
- raw_preflight = self._preflight_record_update_with_auto_view(**preflight_kwargs)
4642
+ raw_preflight = self._preflight_record_update_with_auto_view(
4643
+ profile=profile,
4644
+ app_key=app_key,
4645
+ record_id=record_id,
4646
+ fields=fields,
4647
+ force_refresh_form=False,
4648
+ )
5098
4649
  preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
5099
4650
  normalized_payload = self._record_write_normalized_payload(
5100
4651
  operation="update",
@@ -5300,9 +4851,6 @@ class RecordTools(ToolBase):
5300
4851
  item: JSONObject = {
5301
4852
  "resource": data.get("resource"),
5302
4853
  "status": response.get("status"),
5303
- "write_executed": bool(response.get("write_executed")),
5304
- "safe_to_retry": bool(response.get("safe_to_retry", True)),
5305
- "verification_status": response.get("verification_status", "not_requested"),
5306
4854
  "verification": data.get("verification"),
5307
4855
  "field_errors": cast(list[JSONObject], data.get("field_errors", [])),
5308
4856
  "confirmation_requests": cast(list[JSONObject], data.get("confirmation_requests", [])),
@@ -5312,7 +4860,7 @@ class RecordTools(ToolBase):
5312
4860
  if isinstance(update_route, dict):
5313
4861
  item["update_route"] = update_route
5314
4862
  tried_routes = response.get("tried_routes")
5315
- if isinstance(tried_routes, list) and (output_profile == "verbose" or response.get("status") != "success"):
4863
+ if isinstance(tried_routes, list):
5316
4864
  item["tried_routes"] = tried_routes
5317
4865
  blockers = data.get("blockers")
5318
4866
  if isinstance(blockers, list) and blockers:
@@ -5334,7 +4882,6 @@ class RecordTools(ToolBase):
5334
4882
  app_key: str,
5335
4883
  record_id: int,
5336
4884
  fields: JSONObject,
5337
- preferred_view_id: str | None = None,
5338
4885
  force_refresh_form: bool,
5339
4886
  ) -> JSONObject:
5340
4887
  """执行内部辅助逻辑。"""
@@ -5342,25 +4889,6 @@ class RecordTools(ToolBase):
5342
4889
  request_route = self._request_route_payload(context)
5343
4890
  def build_once(*, effective_force_refresh: bool) -> JSONObject:
5344
4891
  candidate_routes = self._candidate_update_views(profile, context, app_key)
5345
- normalized_preferred_view_id = _normalize_optional_text(preferred_view_id)
5346
- if normalized_preferred_view_id:
5347
- preferred_route = next(
5348
- (
5349
- route
5350
- for route in candidate_routes
5351
- if route.view_id == normalized_preferred_view_id
5352
- ),
5353
- None,
5354
- )
5355
- if preferred_route is None:
5356
- raise_tool_error(
5357
- QingflowApiError.config_error(
5358
- f"view_id '{normalized_preferred_view_id}' is not an accessible update candidate"
5359
- )
5360
- )
5361
- candidate_routes = [preferred_route]
5362
- else:
5363
- candidate_routes = _prefer_custom_update_routes(candidate_routes)
5364
4892
  probes = self._probe_candidate_record_contexts(
5365
4893
  context,
5366
4894
  app_key=app_key,
@@ -5574,24 +5102,41 @@ class RecordTools(ToolBase):
5574
5102
  "data": first_confirmation_plan,
5575
5103
  }
5576
5104
 
5577
- if normalized_preferred_view_id and first_blocked_plan is not None:
5578
- first_blocked_plan["view_probe_summary"] = probe_summary
5579
- first_blocked_plan["record_context_probe"] = probe_summary
5105
+ union_plan = self._build_record_update_union_preflight(
5106
+ profile=profile,
5107
+ context=context,
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
5580
5127
  return {
5581
5128
  "profile": profile,
5582
5129
  "ws_id": session_profile.selected_ws_id,
5583
5130
  "ok": True,
5584
5131
  "request_route": request_route,
5585
- "data": first_blocked_plan,
5132
+ "data": union_plan,
5586
5133
  }
5587
5134
 
5588
5135
  blocked_data = self._build_auto_view_blocked_preflight_data(
5589
5136
  app_key=app_key,
5590
5137
  record_id=record_id,
5591
5138
  blockers=["NO_SINGLE_VIEW_CAN_UPDATE_ALL_FIELDS"],
5592
- warnings=[
5593
- "record_update requires one executable frontend route for the full payload; it does not merge writable fields across multiple views."
5594
- ],
5139
+ warnings=[],
5595
5140
  recommended_next_actions=[
5596
5141
  "Call record_update_schema_get first to inspect the overall writable field set for this record.",
5597
5142
  "Reduce the update payload until all requested fields fit inside one matched accessible view.",
@@ -5638,7 +5183,6 @@ class RecordTools(ToolBase):
5638
5183
  union_writable_field_ids: set[int] = set()
5639
5184
  union_visible_question_ids: set[int] = set()
5640
5185
  matched_view_payloads: list[JSONObject] = []
5641
- union_index: FieldIndex | None = None
5642
5186
 
5643
5187
  for candidate in matched_routes:
5644
5188
  browse_scope = self._build_browse_write_scope(
@@ -5648,13 +5192,11 @@ class RecordTools(ToolBase):
5648
5192
  candidate,
5649
5193
  force_refresh=force_refresh_form,
5650
5194
  )
5651
- browse_index = cast(FieldIndex, browse_scope["index"])
5652
- union_index = browse_index if union_index is None else _merge_field_indexes(union_index, browse_index)
5653
5195
  union_writable_field_ids.update(cast(set[int], browse_scope["writable_field_ids"]))
5654
5196
  union_visible_question_ids.update(cast(set[int], browse_scope["visible_question_ids"]))
5655
5197
  matched_view_payloads.append(_accessible_view_payload(candidate))
5656
5198
 
5657
- if union_index is None or (not union_writable_field_ids and not union_visible_question_ids):
5199
+ if not union_writable_field_ids and not union_visible_question_ids:
5658
5200
  return None
5659
5201
 
5660
5202
  plan_data = self._build_record_write_preflight(
@@ -5671,9 +5213,10 @@ class RecordTools(ToolBase):
5671
5213
  view_key=None,
5672
5214
  view_name=None,
5673
5215
  existing_answers_override=current_answers,
5674
- field_index_override=union_index,
5675
5216
  )
5676
5217
 
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)
5677
5220
  validation = cast(JSONObject, plan_data.get("validation", {}))
5678
5221
  invalid_fields = cast(list[JSONObject], validation.get("invalid_fields", []))
5679
5222
  missing_required_fields = cast(list[JSONObject], validation.get("missing_required_fields", []))
@@ -5684,21 +5227,12 @@ class RecordTools(ToolBase):
5684
5227
  invalid_fields.extend(
5685
5228
  self._validate_view_scoped_subtable_answers(
5686
5229
  normalized_answers=cast(list[JSONObject], plan_data.get("normalized_answers", [])),
5687
- full_index=union_index,
5688
- selector_index=union_index,
5230
+ full_index=app_index,
5231
+ selector_index=app_index,
5689
5232
  visible_question_ids=union_visible_question_ids,
5690
5233
  )
5691
5234
  )
5692
5235
 
5693
- readonly_or_system_fields = [
5694
- item
5695
- for item in readonly_or_system_fields
5696
- if not (
5697
- isinstance(item, dict)
5698
- and (que_id := _coerce_count(item.get("que_id"))) is not None
5699
- and que_id in union_writable_field_ids
5700
- )
5701
- ]
5702
5236
  existing_readonly_ids = {
5703
5237
  str(_coerce_count(item.get("que_id")))
5704
5238
  for item in readonly_or_system_fields
@@ -5862,13 +5396,7 @@ class RecordTools(ToolBase):
5862
5396
  view_type=None,
5863
5397
  )
5864
5398
  )
5865
- try:
5866
- view_items = self._get_view_list(profile, context, app_key)
5867
- except QingflowApiError as exc:
5868
- if not _is_record_permission_denied_error(exc):
5869
- raise
5870
- view_items = []
5871
- for item in view_items:
5399
+ for item in self._get_view_list(profile, context, app_key):
5872
5400
  if not isinstance(item, dict):
5873
5401
  continue
5874
5402
  view_key = _normalize_optional_text(item.get("viewKey"))
@@ -5925,9 +5453,7 @@ class RecordTools(ToolBase):
5925
5453
  return payload
5926
5454
 
5927
5455
  def _is_record_context_route_miss(self, error: QingflowApiError) -> bool:
5928
- if is_auth_like_error(error):
5929
- return False
5930
- if backend_code_int(error) in {40002, 40023, 40027, 40038, 404}:
5456
+ if error.backend_code in {40002, 40023, 40027, 40038, 404}:
5931
5457
  return True
5932
5458
  if error.http_status == 404:
5933
5459
  return True
@@ -5957,12 +5483,11 @@ class RecordTools(ToolBase):
5957
5483
  used_list_type = None
5958
5484
  else:
5959
5485
  used_list_type = resolved_view.list_type if resolved_view.list_type is not None else DEFAULT_RECORD_LIST_TYPE
5960
- role = _record_detail_role_for_list_type(used_list_type)
5961
5486
  record = self.backend.request(
5962
5487
  "GET",
5963
5488
  context,
5964
5489
  f"/app/{app_key}/apply/{apply_id}",
5965
- params={"role": role, "listType": used_list_type},
5490
+ params={"role": 1, "listType": used_list_type},
5966
5491
  )
5967
5492
  answers = record.get("answers") if isinstance(record, dict) else None
5968
5493
  normalized_answers = [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []
@@ -5992,8 +5517,6 @@ class RecordTools(ToolBase):
5992
5517
  error_payload=None,
5993
5518
  )
5994
5519
  except QingflowApiError as exc:
5995
- if not self._is_record_context_route_miss(exc):
5996
- raise
5997
5520
  return RecordContextRouteProbe(
5998
5521
  route=resolved_view,
5999
5522
  answer_list=None,
@@ -6065,7 +5588,7 @@ class RecordTools(ToolBase):
6065
5588
  ]
6066
5589
 
6067
5590
  def _looks_like_generic_record_update_backend_failure(self, exc: QingflowApiError) -> bool:
6068
- if backend_code_int(exc) == 500:
5591
+ if exc.backend_code == 500:
6069
5592
  return True
6070
5593
  if exc.http_status is not None and exc.http_status >= 500:
6071
5594
  return True
@@ -6190,15 +5713,12 @@ class RecordTools(ToolBase):
6190
5713
  app_key: str,
6191
5714
  record_id: Any | None = None,
6192
5715
  record_ids: list[Any] | None = None,
6193
- view_id: str | None = None,
6194
- list_type: int | None = None,
6195
5716
  output_profile: str = "normal",
6196
5717
  ) -> JSONObject:
6197
5718
  """执行记录相关逻辑。"""
6198
5719
  normalized_output_profile = self._normalize_public_output_profile(output_profile)
6199
5720
  if not app_key:
6200
5721
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
6201
- delete_list_type = self._resolve_record_delete_list_type(view_id=view_id, list_type=list_type)
6202
5722
  normalized_record_ids: list[int] = []
6203
5723
  for index, item in enumerate(record_ids or []):
6204
5724
  normalized_record_ids.append(normalize_positive_id_int(item, field_name=f"record_ids[{index}]"))
@@ -6218,63 +5738,13 @@ class RecordTools(ToolBase):
6218
5738
  "record_ids": [stringify_backend_id(item) for item in delete_ids],
6219
5739
  "answers": [],
6220
5740
  "submit_type": 1,
6221
- "selection": {"view_id": view_id, "list_type": delete_list_type},
6222
5741
  }
6223
5742
  return self._record_delete_public_batch(
6224
5743
  profile=profile,
6225
- app_key=app_key,
6226
- delete_ids=delete_ids,
6227
- list_type=delete_list_type,
6228
- normalized_payload=normalized_payload,
6229
- output_profile=normalized_output_profile,
6230
- )
6231
-
6232
- def _resolve_record_delete_list_type(self, *, view_id: str | None, list_type: int | None) -> int:
6233
- normalized_view_id = _normalize_optional_text(view_id)
6234
- if normalized_view_id:
6235
- if normalized_view_id.startswith("custom:"):
6236
- raise_tool_error(
6237
- QingflowApiError.config_error(
6238
- "record_delete does not support custom view deletion; the backend delete route accepts system listType only",
6239
- details={
6240
- "error_code": "RECORD_DELETE_CUSTOM_VIEW_UNSUPPORTED",
6241
- "view_id": normalized_view_id,
6242
- "fix_hint": (
6243
- "Use a system view_id from app_get.accessible_views, or resolve target record_ids with "
6244
- "record list/get first and retry delete without a custom view selector."
6245
- ),
6246
- },
6247
- )
6248
- )
6249
- if not normalized_view_id.startswith("system:"):
6250
- raise_tool_error(QingflowApiError.config_error("view_id must start with system: or custom:"))
6251
- mapped_list_type = SYSTEM_VIEW_ID_TO_LIST_TYPE.get(normalized_view_id)
6252
- if mapped_list_type is None:
6253
- raise_tool_error(QingflowApiError.config_error(f"unsupported view_id '{normalized_view_id}'"))
6254
- return mapped_list_type
6255
- if list_type is not None:
6256
- normalized_list_type = int(list_type)
6257
- if normalized_list_type not in SYSTEM_VIEW_LIST_TYPES:
6258
- raise_tool_error(
6259
- QingflowApiError.config_error(
6260
- "record_delete list_type must map to a supported system view",
6261
- details={
6262
- "error_code": "RECORD_DELETE_SYSTEM_VIEW_REQUIRED",
6263
- "list_type": normalized_list_type,
6264
- "supported_list_types": sorted(SYSTEM_VIEW_LIST_TYPES),
6265
- "fix_hint": "Pass a system view_id from app_get.accessible_views instead of an arbitrary list_type.",
6266
- },
6267
- )
6268
- )
6269
- return normalized_list_type
6270
- raise_tool_error(
6271
- QingflowApiError.config_error(
6272
- "record_delete requires a system view_id or list_type; deleting without frontend list context is ambiguous",
6273
- details={
6274
- "error_code": "RECORD_DELETE_VIEW_REQUIRED",
6275
- "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.",
6276
- },
6277
- )
5744
+ app_key=app_key,
5745
+ delete_ids=delete_ids,
5746
+ normalized_payload=normalized_payload,
5747
+ output_profile=normalized_output_profile,
6278
5748
  )
6279
5749
 
6280
5750
  def _record_delete_public_batch(
@@ -6283,7 +5753,6 @@ class RecordTools(ToolBase):
6283
5753
  profile: str,
6284
5754
  app_key: str,
6285
5755
  delete_ids: list[int],
6286
- list_type: int,
6287
5756
  normalized_payload: JSONObject,
6288
5757
  output_profile: str,
6289
5758
  ) -> JSONObject:
@@ -6293,7 +5762,7 @@ class RecordTools(ToolBase):
6293
5762
  for index, delete_id in enumerate(delete_ids):
6294
5763
  record_id_text = stringify_backend_id(delete_id)
6295
5764
  try:
6296
- raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=[delete_id], list_type=list_type)
5765
+ raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=[delete_id])
6297
5766
  request_route = cast(JSONObject, raw_apply.get("request_route")) if isinstance(raw_apply.get("request_route"), dict) else request_route
6298
5767
  ws_id = raw_apply.get("ws_id", ws_id)
6299
5768
  single_payload = {
@@ -6302,7 +5771,6 @@ class RecordTools(ToolBase):
6302
5771
  "record_ids": [record_id_text],
6303
5772
  "answers": [],
6304
5773
  "submit_type": 1,
6305
- "selection": normalized_payload.get("selection"),
6306
5774
  }
6307
5775
  single_response = self._record_write_apply_response(
6308
5776
  raw_apply,
@@ -6585,13 +6053,12 @@ class RecordTools(ToolBase):
6585
6053
  preflight=raw_preflight,
6586
6054
  )
6587
6055
 
6588
- if view_key is not None or view_name is not None:
6056
+ if uses_view_scope:
6589
6057
  raise_tool_error(
6590
6058
  QingflowApiError.config_error(
6591
- "delete does not support custom view selectors; use a system view_id/list_type or resolve target record_ids first"
6059
+ "delete does not accept view selectors yet; resolve target record_ids from the selected view first, then call delete by record_id/record_ids"
6592
6060
  )
6593
6061
  )
6594
- delete_list_type = self._resolve_record_delete_list_type(view_id=view_id, list_type=list_type)
6595
6062
  if normalized_values or normalized_set:
6596
6063
  raise_tool_error(QingflowApiError.config_error("delete must not include values or set"))
6597
6064
  delete_ids = normalized_record_ids or ([record_id] if record_id is not None and record_id > 0 else [])
@@ -6603,9 +6070,8 @@ class RecordTools(ToolBase):
6603
6070
  "record_ids": delete_ids,
6604
6071
  "answers": [],
6605
6072
  "submit_type": submit_type_value,
6606
- "selection": {"view_id": view_id, "list_type": delete_list_type},
6607
6073
  }
6608
- raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=delete_ids, list_type=delete_list_type)
6074
+ raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=delete_ids)
6609
6075
  return self._record_write_apply_response(
6610
6076
  raw_apply,
6611
6077
  operation="delete",
@@ -6755,9 +6221,7 @@ class RecordTools(ToolBase):
6755
6221
  or _normalize_optional_text(payload.get("appName"))
6756
6222
  or _normalize_optional_text(payload.get("appTitle"))
6757
6223
  )
6758
- except QingflowApiError as exc:
6759
- if is_auth_like_error(exc):
6760
- raise
6224
+ except QingflowApiError:
6761
6225
  name = None
6762
6226
  self._app_name_cache[cache_key] = name
6763
6227
  return name
@@ -6911,9 +6375,7 @@ class RecordTools(ToolBase):
6911
6375
  try:
6912
6376
  result = self.backend.request("GET", context, "/data/baseInfo", params={"queId": field.que_id})
6913
6377
  payload = result if isinstance(result, dict) else None
6914
- except QingflowApiError as exc:
6915
- if is_auth_like_error(exc):
6916
- raise
6378
+ except QingflowApiError:
6917
6379
  payload = None
6918
6380
  self._relation_base_info_cache[cache_key] = payload or {}
6919
6381
  return payload
@@ -7186,26 +6648,6 @@ class RecordTools(ToolBase):
7186
6648
  or bool(fields)
7187
6649
  )
7188
6650
 
7189
- def _member_candidate_static_preview_should_use_backend(self, field: FormField) -> bool:
7190
- """Return true when the frontend field-scope endpoint is safer than directory expansion."""
7191
- scope = field.member_select_scope if isinstance(field.member_select_scope, dict) else {}
7192
- if field.member_select_scope_type != 2:
7193
- return False
7194
- return bool(
7195
- _scope_has_dynamic_or_external(scope)
7196
- or list(scope.get("depart") or [])
7197
- or list(scope.get("role") or [])
7198
- )
7199
-
7200
- def _department_candidate_static_preview_should_use_backend(self, field: FormField) -> bool:
7201
- """Return true when static preview would otherwise need ContactAuth-only directory APIs."""
7202
- scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
7203
- if field.dept_select_scope_type != 2:
7204
- return False
7205
- if _scope_has_dynamic_or_external(scope):
7206
- return True
7207
- return bool(_normalize_bool(scope.get("includeSubDeparts")) or not list(scope.get("depart") or []))
7208
-
7209
6651
  def _build_candidate_lookup_state(
7210
6652
  self,
7211
6653
  profile: str,
@@ -7224,9 +6666,7 @@ class RecordTools(ToolBase):
7224
6666
  if apply_id is not None:
7225
6667
  try:
7226
6668
  base_answers = self._load_record_answers_for_preflight(context, app_key=app_key, apply_id=apply_id)
7227
- except QingflowApiError as exc:
7228
- if not _is_optional_record_auxiliary_lookup_error(exc):
7229
- raise
6669
+ except QingflowApiError:
7230
6670
  context_complete = False
7231
6671
  state = LookupResolutionState(
7232
6672
  operation="update" if apply_id is not None else "insert",
@@ -7716,16 +7156,15 @@ class RecordTools(ToolBase):
7716
7156
  )
7717
7157
  if configured_candidate is not None:
7718
7158
  self._merge_department_candidate(merged, configured_candidate)
7719
- if include_sub:
7720
- for dept in self._list_departments_by_scope(context, dept_id=dept_id, include_sub_departments=True):
7721
- normalized = _normalize_candidate_department(
7722
- dept,
7723
- source_kind="department",
7724
- source_id=dept_id,
7725
- source_value=dept_name,
7726
- )
7727
- if normalized is not None:
7728
- self._merge_department_candidate(merged, normalized)
7159
+ for dept in self._list_departments_by_scope(context, dept_id=dept_id, include_sub_departments=include_sub):
7160
+ normalized = _normalize_candidate_department(
7161
+ dept,
7162
+ source_kind="department",
7163
+ source_id=dept_id,
7164
+ source_value=dept_name,
7165
+ )
7166
+ if normalized is not None:
7167
+ self._merge_department_candidate(merged, normalized)
7729
7168
  filtered = _filter_department_candidates(list(merged.values()), keyword)
7730
7169
  filtered.sort(key=lambda item: (_normalize_optional_text(item.get("value")) or "", _coerce_count(item.get("id")) or 0))
7731
7170
  return filtered
@@ -8866,10 +8305,22 @@ class RecordTools(ToolBase):
8866
8305
  field_index_override: FieldIndex | None = None,
8867
8306
  ) -> JSONObject:
8868
8307
  """执行内部辅助逻辑。"""
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)
8869
8320
  normalized_fields = fields or {}
8870
8321
  normalized_answers_input = answers or []
8871
8322
  resolved_view: AccessibleViewRoute | None = None
8872
- selector_index: FieldIndex | None = field_index_override
8323
+ selector_index = index
8873
8324
  browse_writable_field_ids: set[int] = set()
8874
8325
  visible_question_ids: set[int] = set()
8875
8326
  if any(item is not None for item in (view_id, list_type, view_key, view_name)):
@@ -8895,31 +8346,6 @@ class RecordTools(ToolBase):
8895
8346
  visible_question_ids = cast(set[int], browse_scope["visible_question_ids"])
8896
8347
  else:
8897
8348
  compatibility_warnings = []
8898
- if field_index_override is not None:
8899
- base_index = field_index_override
8900
- question_relations: list[JSONObject] = []
8901
- runtime_linked_field_ids: set[int] = set()
8902
- index = base_index
8903
- elif operation == "update" and resolved_view is not None:
8904
- base_index = cast(FieldIndex, selector_index)
8905
- question_relations = []
8906
- runtime_linked_field_ids = set()
8907
- index = base_index
8908
- else:
8909
- schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
8910
- base_index = _build_applicant_top_level_field_index(schema)
8911
- question_relations = _collect_question_relations(schema)
8912
- runtime_linked_field_ids = _collect_linked_required_field_ids(question_relations)
8913
- runtime_linked_field_ids.update(_collect_option_linked_field_ids(base_index))
8914
- index = base_index
8915
- if operation == "create":
8916
- linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
8917
- schema,
8918
- linked_field_ids=runtime_linked_field_ids,
8919
- )
8920
- index = _merge_field_indexes(base_index, linked_hidden_index)
8921
- if selector_index is None:
8922
- selector_index = index
8923
8349
  resolved_fields = self._collect_write_plan_field_refs(fields=normalized_fields, answers=normalized_answers_input, index=selector_index)
8924
8350
  support_matrix = _summarize_write_support(resolved_fields)
8925
8351
  invalid_fields: list[JSONObject] = []
@@ -8963,9 +8389,7 @@ class RecordTools(ToolBase):
8963
8389
  apply_id=apply_id,
8964
8390
  )
8965
8391
  existing_answers_loaded = True
8966
- except QingflowApiError as exc:
8967
- if not _is_optional_record_auxiliary_lookup_error(exc):
8968
- raise
8392
+ except QingflowApiError:
8969
8393
  validation_warnings.append(
8970
8394
  "update preflight could not load the current record; required-field completeness and dynamic lookup context were not fully revalidated."
8971
8395
  )
@@ -9554,7 +8978,7 @@ class RecordTools(ToolBase):
9554
8978
  break
9555
8979
  except QingflowApiError as exc:
9556
8980
  last_error = exc
9557
- if _is_record_permission_denied_error(exc):
8981
+ if exc.backend_code == 40002:
9558
8982
  continue
9559
8983
  raise
9560
8984
  if result is None:
@@ -9657,21 +9081,7 @@ class RecordTools(ToolBase):
9657
9081
  normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
9658
9082
 
9659
9083
  def runner(session_profile, context):
9660
- needs_index = verify_write or bool(fields) or _answers_need_resolution(answers or [])
9661
- update_index = None
9662
- if needs_index:
9663
- update_index = (
9664
- self._get_system_browse_field_index(
9665
- profile,
9666
- context,
9667
- app_key,
9668
- list_type=DEFAULT_RECORD_LIST_TYPE,
9669
- force_refresh=force_refresh_form,
9670
- )
9671
- if role == 1
9672
- else self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
9673
- )
9674
- index = update_index if verify_write else None
9084
+ index = self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form) if verify_write else None
9675
9085
  normalized_answers = self._resolve_answers(
9676
9086
  profile,
9677
9087
  context,
@@ -9679,7 +9089,6 @@ class RecordTools(ToolBase):
9679
9089
  answers=answers or [],
9680
9090
  fields=fields or {},
9681
9091
  force_refresh_form=force_refresh_form,
9682
- field_index_override=update_index,
9683
9092
  )
9684
9093
  self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
9685
9094
  try:
@@ -9733,14 +9142,13 @@ class RecordTools(ToolBase):
9733
9142
  def record_delete(self, *, profile: str, app_key: str, apply_id: int, list_type: int) -> JSONObject:
9734
9143
  """执行记录相关逻辑。"""
9735
9144
  normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
9736
- delete_list_type = self._resolve_record_delete_list_type(view_id=None, list_type=list_type)
9737
9145
 
9738
9146
  def runner(session_profile, context):
9739
9147
  result = self.backend.request(
9740
9148
  "DELETE",
9741
9149
  context,
9742
9150
  f"/app/{app_key}/apply",
9743
- json_body={"type": delete_list_type, "applyIds": [normalized_apply_id]},
9151
+ json_body={"type": list_type, "applyIds": [normalized_apply_id]},
9744
9152
  )
9745
9153
  return self._attach_human_review_notice(
9746
9154
  {
@@ -9749,7 +9157,6 @@ class RecordTools(ToolBase):
9749
9157
  "request_route": self._request_route_payload(context),
9750
9158
  "app_key": app_key,
9751
9159
  "apply_id": normalized_apply_id,
9752
- "list_type": delete_list_type,
9753
9160
  "result": result,
9754
9161
  },
9755
9162
  operation="delete",
@@ -9794,7 +9201,7 @@ class RecordTools(ToolBase):
9794
9201
  "GET",
9795
9202
  context,
9796
9203
  f"/app/{app_key}/apply/{apply_id}",
9797
- params={"role": _record_detail_role_for_list_type(list_type), "listType": list_type},
9204
+ params={"role": 1, "listType": list_type},
9798
9205
  )
9799
9206
  answers = result.get("answers") if isinstance(result, dict) else None
9800
9207
  answer_list = answers if isinstance(answers, list) else []
@@ -10153,7 +9560,7 @@ class RecordTools(ToolBase):
10153
9560
  used_list_type: int | None = None
10154
9561
  if view_selection is not None:
10155
9562
  fallback_list_types = [view_route.list_type if view_route.list_type is not None else DEFAULT_RECORD_LIST_TYPE]
10156
- elif view_route.list_type is not None:
9563
+ elif view_route.list_type is not None and view_route.list_type != DEFAULT_RECORD_LIST_TYPE:
10157
9564
  fallback_list_types = [view_route.list_type]
10158
9565
  else:
10159
9566
  fallback_list_types = [DEFAULT_RECORD_LIST_TYPE, 14, 1, 2, 12]
@@ -10384,7 +9791,7 @@ class RecordTools(ToolBase):
10384
9791
  try:
10385
9792
  payload = self.backend.request("GET", context, f"/view/{view_key}/viewConfig")
10386
9793
  except QingflowApiError as exc:
10387
- if _is_optional_schema_permission_error(exc):
9794
+ if exc.backend_code in {40002, 40027, 404} or exc.http_status == 404:
10388
9795
  self._view_config_cache[cache_key] = None
10389
9796
  return None
10390
9797
  raise
@@ -10505,12 +9912,7 @@ class RecordTools(ToolBase):
10505
9912
  )
10506
9913
  normalized = _normalize_data_list_base_info_schema(payload)
10507
9914
  if not isinstance(normalized.get("formQues"), list) or not normalized.get("formQues"):
10508
- try:
10509
- return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
10510
- except QingflowApiError as exc:
10511
- if not _is_optional_schema_permission_error(exc):
10512
- raise
10513
- return normalized
9915
+ return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
10514
9916
  self._form_cache[cache_key] = normalized
10515
9917
  return normalized
10516
9918
 
@@ -10542,16 +9944,8 @@ class RecordTools(ToolBase):
10542
9944
  cache_key = (profile, f"view:{view_key}", "browse_view_base_info", None)
10543
9945
  if not force_refresh and cache_key in self._form_cache:
10544
9946
  return self._form_cache[cache_key]
10545
- try:
10546
- payload = self.backend.request("GET", context, f"/view/{view_key}/apply/baseInfo")
10547
- normalized = _normalize_data_list_base_info_schema(payload)
10548
- form_ques = normalized.get("formQues")
10549
- if not isinstance(form_ques, list) or not form_ques:
10550
- normalized = self._get_view_form_schema(profile, context, view_key, force_refresh=force_refresh)
10551
- except QingflowApiError as exc:
10552
- if not _is_optional_schema_permission_error(exc):
10553
- raise
10554
- normalized = self._get_view_form_schema(profile, context, view_key, force_refresh=force_refresh)
9947
+ payload = self.backend.request("GET", context, f"/view/{view_key}/apply/baseInfo")
9948
+ normalized = _normalize_data_list_base_info_schema(payload)
10555
9949
  self._form_cache[cache_key] = normalized
10556
9950
  return normalized
10557
9951
 
@@ -10607,6 +10001,22 @@ class RecordTools(ToolBase):
10607
10001
  force_refresh: bool,
10608
10002
  ) -> JSONObject:
10609
10003
  """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
+
10610
10020
  if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
10611
10021
  schema = self._get_custom_view_browse_schema(
10612
10022
  profile,
@@ -10615,16 +10025,6 @@ class RecordTools(ToolBase):
10615
10025
  force_refresh=force_refresh,
10616
10026
  )
10617
10027
  index = _build_top_level_field_index(schema)
10618
- visible_question_ids = {field.que_id for field in index.by_id.values()}
10619
- return {
10620
- "index": index,
10621
- "writable_field_ids": {
10622
- field.que_id
10623
- for field in index.by_id.values()
10624
- if bool(self._schema_write_hints(field)["writable"])
10625
- },
10626
- "visible_question_ids": visible_question_ids,
10627
- }
10628
10028
  elif resolved_view is not None and resolved_view.kind == "system" and resolved_view.list_type is not None:
10629
10029
  schema = self._get_system_browse_base_info_schema(
10630
10030
  profile,
@@ -10634,26 +10034,34 @@ class RecordTools(ToolBase):
10634
10034
  force_refresh=force_refresh,
10635
10035
  )
10636
10036
  index = _build_top_level_field_index(schema)
10637
- visible_question_ids = {field.que_id for field in index.by_id.values()}
10638
- return {
10639
- "index": index,
10640
- "writable_field_ids": {
10641
- field.que_id
10642
- for field in index.by_id.values()
10643
- if bool(self._schema_write_hints(field)["writable"])
10644
- },
10645
- "visible_question_ids": visible_question_ids,
10646
- }
10037
+ else:
10038
+ index = applicant_index or _build_top_level_field_index(
10039
+ self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
10040
+ )
10647
10041
 
10648
- applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
10649
- visible_question_ids = {field.que_id for field in applicant_index.by_id.values()}
10650
- return {
10651
- "index": applicant_index,
10652
- "writable_field_ids": {
10042
+ if applicant_index is not None and index.by_id:
10043
+ enriched_fields = [
10044
+ _enrich_read_field_from_applicant(field, applicant_index.by_id.get(str(field.que_id)))
10045
+ for field in index.by_id.values()
10046
+ ]
10047
+ index = _build_top_level_field_index({"formQues": [[_form_field_to_question(field) for field in enriched_fields]]})
10048
+
10049
+ visible_question_ids = {field.que_id for field in index.by_id.values()}
10050
+ if applicant_index is None:
10051
+ writable_field_ids = {
10653
10052
  field.que_id
10654
- for field in applicant_index.by_id.values()
10053
+ for field in index.by_id.values()
10655
10054
  if bool(self._schema_write_hints(field)["writable"])
10656
- },
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,
10657
10065
  "visible_question_ids": visible_question_ids,
10658
10066
  }
10659
10067
 
@@ -10667,13 +10075,23 @@ class RecordTools(ToolBase):
10667
10075
  force_refresh: bool,
10668
10076
  ) -> JSONObject:
10669
10077
  """执行内部辅助逻辑。"""
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
+ }
10670
10093
  if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
10671
- schema = self._get_custom_view_browse_schema(
10672
- profile,
10673
- context,
10674
- resolved_view.view_selection.view_key,
10675
- force_refresh=force_refresh,
10676
- )
10094
+ schema = self._get_view_form_schema(profile, context, resolved_view.view_selection.view_key, force_refresh=force_refresh)
10677
10095
  index = _build_top_level_field_index(schema)
10678
10096
  visible_question_ids = self._get_view_question_ids(profile, context, resolved_view.view_selection.view_key)
10679
10097
  if not visible_question_ids:
@@ -10689,12 +10107,6 @@ class RecordTools(ToolBase):
10689
10107
  index = _build_top_level_field_index(schema)
10690
10108
  visible_question_ids = _question_ids_from_schema(schema)
10691
10109
  else:
10692
- applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
10693
- applicant_writable_field_ids = {
10694
- field.que_id
10695
- for field in applicant_index.by_id.values()
10696
- if bool(self._schema_write_hints(field)["writable"])
10697
- }
10698
10110
  index = applicant_index or _build_top_level_field_index(
10699
10111
  self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
10700
10112
  )
@@ -10713,13 +10125,43 @@ class RecordTools(ToolBase):
10713
10125
  "visible_question_ids": set(visible_question_ids),
10714
10126
  }
10715
10127
 
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)
10716
10162
  return {
10717
- "index": index,
10718
- "writable_field_ids": {
10719
- field.que_id
10720
- for field in index.by_id.values()
10721
- if bool(self._schema_write_hints(field)["writable"])
10722
- },
10163
+ "index": _build_top_level_field_index({"formQues": [[_form_field_to_question(field) for field in augmented_fields]]}),
10164
+ "writable_field_ids": writable_field_ids,
10723
10165
  "visible_question_ids": visible_question_ids,
10724
10166
  }
10725
10167
 
@@ -10784,7 +10226,7 @@ class RecordTools(ToolBase):
10784
10226
  try:
10785
10227
  payload = self.backend.request("GET", context, f"/view/{view_key}/question")
10786
10228
  except QingflowApiError as exc:
10787
- if _is_record_permission_denied_error(exc):
10229
+ if exc.backend_code in {40002, 40027}:
10788
10230
  return set()
10789
10231
  raise
10790
10232
  if not isinstance(payload, list):
@@ -10828,7 +10270,7 @@ class RecordTools(ToolBase):
10828
10270
  )
10829
10271
  return True
10830
10272
  except QingflowApiError as exc:
10831
- if _is_record_permission_denied_error(exc):
10273
+ if exc.backend_code in {40002, 40027}:
10832
10274
  return False
10833
10275
  raise
10834
10276
 
@@ -10981,12 +10423,7 @@ class RecordTools(ToolBase):
10981
10423
  requested_name = _normalize_optional_text(view_name)
10982
10424
  if requested_key is None and requested_name is None:
10983
10425
  return None
10984
- try:
10985
- views = self._get_view_list(profile, context, app_key)
10986
- except QingflowApiError as exc:
10987
- if requested_key is None or not _is_record_permission_denied_error(exc):
10988
- raise
10989
- views = []
10426
+ views = self._get_view_list(profile, context, app_key)
10990
10427
  selected: JSONObject | None = None
10991
10428
  if requested_key is not None:
10992
10429
  selected = next((item for item in views if _normalize_optional_text(item.get("viewKey")) == requested_key), None)
@@ -11184,11 +10621,9 @@ class RecordTools(ToolBase):
11184
10621
 
11185
10622
  def _should_retry_list_type_fallback(self, error: QingflowApiError) -> bool:
11186
10623
  """执行内部辅助逻辑。"""
11187
- if is_auth_like_error(error):
11188
- return False
11189
- if backend_code_int(error) in {40002, 40027, 404}:
10624
+ if error.backend_code in {40002, 40027, 404}:
11190
10625
  return True
11191
- if error.http_status == 404:
10626
+ if error.http_status in {404, 500}:
11192
10627
  return True
11193
10628
  return False
11194
10629
 
@@ -11970,8 +11405,6 @@ class RecordTools(ToolBase):
11970
11405
  schema: JSONObject = {}
11971
11406
  if isinstance(raw.get("subQuestions"), list):
11972
11407
  schema["formQues"] = [raw["subQuestions"]]
11973
- elif isinstance(raw.get("subQues"), list):
11974
- schema["formQues"] = [raw["subQues"]]
11975
11408
  elif isinstance(raw.get("innerQuestions"), list):
11976
11409
  schema["formQues"] = raw["innerQuestions"]
11977
11410
  index = _build_field_index(schema)
@@ -12011,70 +11444,6 @@ class RecordTools(ToolBase):
12011
11444
  )
12012
11445
  )
12013
11446
 
12014
- def _candidate_lookup_failed_response(
12015
- self,
12016
- *,
12017
- profile: str,
12018
- session_profile, # type: ignore[no-untyped-def]
12019
- context, # type: ignore[no-untyped-def]
12020
- kind: str,
12021
- error: RecordInputError,
12022
- field: FormField,
12023
- app_key: str,
12024
- record_id_text: str | None,
12025
- workflow_node_id: int | None,
12026
- fields_present: bool,
12027
- keyword: str,
12028
- scope_source: str,
12029
- ) -> JSONObject:
12030
- """Return a structured result when an optional field candidate lookup is unavailable."""
12031
- error_payload = error.to_dict()
12032
- error_details = error_payload.get("details") if isinstance(error_payload.get("details"), dict) else {}
12033
- candidate_error = error_details.get("candidate_error") if isinstance(error_details.get("candidate_error"), dict) else {}
12034
- warning_transport = {
12035
- key: candidate_error.get(key)
12036
- for key in ("backend_code", "http_status", "request_id")
12037
- if candidate_error.get(key) is not None
12038
- }
12039
- selection: JSONObject = {
12040
- "app_key": app_key,
12041
- "field_id": field.que_id,
12042
- "field_title": field.que_title,
12043
- "record_id": record_id_text,
12044
- "workflow_node_id": workflow_node_id,
12045
- "fields_present": fields_present,
12046
- "keyword": keyword,
12047
- "permission_scope": "applicant_node",
12048
- }
12049
- return {
12050
- "profile": profile,
12051
- "ws_id": session_profile.selected_ws_id,
12052
- "ok": False,
12053
- "status": "failed",
12054
- "error_code": error.error_code,
12055
- "message": error.message,
12056
- "request_route": self._request_route_payload(context),
12057
- "warnings": [
12058
- {
12059
- "code": error.error_code,
12060
- "message": error.fix_hint,
12061
- "kind": kind,
12062
- "field_id": field.que_id,
12063
- "field_title": field.que_title,
12064
- **warning_transport,
12065
- }
12066
- ],
12067
- "output_profile": "normal",
12068
- "data": {
12069
- "items": [],
12070
- "pagination": {"returned_items": 0},
12071
- "selection": selection,
12072
- "scope_source": scope_source,
12073
- "fix_hint": error.fix_hint,
12074
- },
12075
- "details": error_details,
12076
- }
12077
-
12078
11447
  def _request_route_payload(self, context) -> JSONObject: # type: ignore[no-untyped-def]
12079
11448
  """执行内部辅助逻辑。"""
12080
11449
  describe_route = getattr(self.backend, "describe_route", None)
@@ -12248,7 +11617,7 @@ class RecordTools(ToolBase):
12248
11617
  selection: JSONObject | None,
12249
11618
  ) -> None:
12250
11619
  """执行内部辅助逻辑。"""
12251
- if not _is_record_permission_denied_error(exc):
11620
+ if exc.backend_code != 40002:
12252
11621
  raise exc
12253
11622
  raise_tool_error(
12254
11623
  QingflowApiError(
@@ -12414,7 +11783,6 @@ class RecordTools(ToolBase):
12414
11783
  response_status = raw_status or "failed"
12415
11784
  update_route = raw_apply.get("update_route") if isinstance(raw_apply.get("update_route"), dict) else None
12416
11785
  tried_routes = raw_apply.get("tried_routes") if isinstance(raw_apply.get("tried_routes"), list) else []
12417
- expose_tried_routes = output_profile == "verbose" or response_status != "success"
12418
11786
  response: JSONObject = {
12419
11787
  "profile": raw_apply.get("profile"),
12420
11788
  "ws_id": raw_apply.get("ws_id"),
@@ -12427,6 +11795,7 @@ class RecordTools(ToolBase):
12427
11795
  "warnings": warnings,
12428
11796
  "output_profile": output_profile,
12429
11797
  "update_route": update_route,
11798
+ "tried_routes": tried_routes,
12430
11799
  "data": {
12431
11800
  "action": {"operation": operation, "executed": True},
12432
11801
  "resource": resource,
@@ -12438,11 +11807,9 @@ class RecordTools(ToolBase):
12438
11807
  "resolved_fields": resolved_fields,
12439
11808
  "human_review": self._record_write_human_review_payload(operation, enabled=human_review),
12440
11809
  "update_route": update_route,
11810
+ "tried_routes": tried_routes,
12441
11811
  },
12442
11812
  }
12443
- if expose_tried_routes:
12444
- response["tried_routes"] = tried_routes
12445
- response["data"]["tried_routes"] = tried_routes
12446
11813
  if record_id is not None:
12447
11814
  response["record_id"] = record_id
12448
11815
  if apply_id is not None:
@@ -12622,7 +11989,7 @@ class RecordTools(ToolBase):
12622
11989
  )
12623
11990
  return errors
12624
11991
 
12625
- def _record_delete_many(self, *, profile: str, app_key: str, record_ids: list[int], list_type: int = DEFAULT_RECORD_LIST_TYPE) -> JSONObject:
11992
+ def _record_delete_many(self, *, profile: str, app_key: str, record_ids: list[int]) -> JSONObject:
12626
11993
  """执行内部辅助逻辑。"""
12627
11994
  if not app_key:
12628
11995
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
@@ -12635,14 +12002,14 @@ class RecordTools(ToolBase):
12635
12002
  "DELETE",
12636
12003
  context,
12637
12004
  f"/app/{app_key}/apply",
12638
- json_body={"type": list_type, "applyIds": normalized_ids},
12005
+ json_body={"type": DEFAULT_RECORD_LIST_TYPE, "applyIds": normalized_ids},
12639
12006
  )
12640
12007
  return {
12641
12008
  "profile": profile,
12642
12009
  "ws_id": session_profile.selected_ws_id,
12643
12010
  "request_route": self._request_route_payload(context),
12644
12011
  "result": result,
12645
- "resource": {"type": "record", "apply_ids": normalized_ids, "list_type": list_type},
12012
+ "resource": {"type": "record", "apply_ids": normalized_ids},
12646
12013
  "ok": True,
12647
12014
  }
12648
12015
 
@@ -13216,30 +12583,6 @@ class RecordTools(ToolBase):
13216
12583
  },
13217
12584
  )
13218
12585
 
13219
- def _candidate_lookup_error(
13220
- self,
13221
- *,
13222
- kind: str,
13223
- field: FormField,
13224
- value: JSONValue,
13225
- error: QingflowApiError,
13226
- ) -> RecordInputError:
13227
- """Build the standard candidate lookup failure without raising it."""
13228
- field_kind = "member" if kind == "member" else "department"
13229
- return RecordInputError(
13230
- message=f"{field_kind} candidates for field '{field.que_title}' could not be loaded",
13231
- error_code=f"{kind.upper()}_CANDIDATE_LOOKUP_FAILED",
13232
- fix_hint=(
13233
- f"Run record_{field_kind}_candidates again after the backend error is resolved, "
13234
- "then choose one returned item exactly."
13235
- ),
13236
- details={
13237
- "field": _field_ref_payload(field),
13238
- "received_value": value,
13239
- "candidate_error": error.to_dict(),
13240
- },
13241
- )
13242
-
13243
12586
  def _candidate_keyword_from_value(
13244
12587
  self,
13245
12588
  value: JSONValue,
@@ -13504,7 +12847,14 @@ class RecordTools(ToolBase):
13504
12847
 
13505
12848
  def _lookup_department_detail(self, context, keyword: str, *, purpose: str) -> JSONObject: # type: ignore[no-untyped-def]
13506
12849
  """执行内部辅助逻辑。"""
13507
- items = self._search_workspace_departments(context, keyword=keyword)
12850
+ payload = self.backend.request(
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 []
13508
12858
  normalized_keyword = keyword.strip()
13509
12859
  exact = [
13510
12860
  item for item in items
@@ -14029,7 +13379,6 @@ class RecordTools(ToolBase):
14029
13379
  normalized_answers: list[JSONObject],
14030
13380
  index: FieldIndex,
14031
13381
  verify_list_type: int = DEFAULT_RECORD_LIST_TYPE,
14032
- verify_role: int | None = None,
14033
13382
  verify_view_key: str | None = None,
14034
13383
  ) -> JSONObject:
14035
13384
  """执行内部辅助逻辑。"""
@@ -14049,20 +13398,14 @@ class RecordTools(ToolBase):
14049
13398
  f"/view/{verify_view_key}/apply/{apply_id}",
14050
13399
  )
14051
13400
  else:
14052
- role = verify_role if verify_role is not None else 1
14053
13401
  record = self.backend.request(
14054
13402
  "GET",
14055
13403
  context,
14056
13404
  f"/app/{app_key}/apply/{apply_id}",
14057
- params={"role": role, "listType": verify_list_type},
13405
+ params={"role": 1, "listType": verify_list_type},
14058
13406
  )
14059
13407
  except QingflowApiError as exc:
14060
13408
  if verify_view_key:
14061
- warning: JSONObject = {
14062
- "code": "WRITE_VERIFY_CUSTOM_VIEW_READBACK_FAILED",
14063
- "message": "Write was sent through a custom view route, but the same view could not be re-read for field-level verification.",
14064
- }
14065
- warning.update(_record_detail_error_warning_fields(exc))
14066
13409
  return {
14067
13410
  "verified": False,
14068
13411
  "verification_mode": "custom_view_record_detail",
@@ -14071,9 +13414,14 @@ class RecordTools(ToolBase):
14071
13414
  "missing_fields": [],
14072
13415
  "empty_fields": [],
14073
13416
  "count_mismatches": [],
14074
- "warnings": [warning],
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
+ }],
14075
13423
  }
14076
- if not _is_record_permission_denied_error(exc):
13424
+ if exc.backend_code != 40002:
14077
13425
  raise
14078
13426
  return self._verify_record_write_result_via_initiated_tasks(
14079
13427
  context,
@@ -14121,7 +13469,6 @@ class RecordTools(ToolBase):
14121
13469
  or len(count_mismatches) > mismatch_before
14122
13470
  ):
14123
13471
  continue
14124
- continue
14125
13472
  expected_value = _canonicalize_answer_value_for_compare(answer, field)
14126
13473
  actual_value = _canonicalize_answer_value_for_compare(actual, field)
14127
13474
  if not _canonical_value_is_empty(expected_value) and _canonical_value_is_empty(actual_value):
@@ -14368,8 +13715,6 @@ def _normalize_data_list_base_info_schema(payload: JSONValue) -> JSONObject:
14368
13715
  if not isinstance(payload, dict):
14369
13716
  return {}
14370
13717
  que_base_infos = payload.get("queBaseInfos")
14371
- if not isinstance(que_base_infos, list) and isinstance(payload.get("formQues"), list):
14372
- que_base_infos = payload.get("formQues")
14373
13718
  if not isinstance(que_base_infos, list):
14374
13719
  return {}
14375
13720
  return {
@@ -14756,44 +14101,6 @@ def _build_answer_backed_field_index(
14756
14101
  )
14757
14102
 
14758
14103
 
14759
- def _merge_subtable_parent_field(primary: FormField, extra: FormField) -> FormField:
14760
- if primary.que_type not in SUBTABLE_QUE_TYPES or extra.que_type not in SUBTABLE_QUE_TYPES:
14761
- return primary
14762
- primary_raw = dict(primary.raw) if isinstance(primary.raw, dict) else {}
14763
- extra_raw = dict(extra.raw) if isinstance(extra.raw, dict) else {}
14764
- primary_subquestions = primary_raw.get("subQuestions")
14765
- extra_subquestions = extra_raw.get("subQuestions")
14766
- if not isinstance(primary_subquestions, list) or not isinstance(extra_subquestions, list):
14767
- return primary
14768
- merged_subquestions = [item for item in primary_subquestions if isinstance(item, dict)]
14769
- seen_ids = {
14770
- _coerce_count(item.get("queId"))
14771
- for item in merged_subquestions
14772
- if isinstance(item, dict) and _coerce_count(item.get("queId")) is not None
14773
- }
14774
- for item in extra_subquestions:
14775
- if not isinstance(item, dict):
14776
- continue
14777
- que_id = _coerce_count(item.get("queId"))
14778
- if que_id is not None and que_id in seen_ids:
14779
- continue
14780
- merged_subquestions.append(item)
14781
- if que_id is not None:
14782
- seen_ids.add(que_id)
14783
- if len(merged_subquestions) == len(primary_subquestions):
14784
- return primary
14785
- merged_raw = dict(primary_raw)
14786
- merged_raw["subQuestions"] = merged_subquestions
14787
- merged_field = _clone_form_field(primary)
14788
- merged_field.raw = merged_raw
14789
- return merged_field
14790
-
14791
-
14792
- def _replace_field_in_lookup(index: dict[str, list[FormField]], field: FormField) -> None:
14793
- for key, fields in list(index.items()):
14794
- index[key] = [field if existing.que_id == field.que_id else existing for existing in fields]
14795
-
14796
-
14797
14104
  def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
14798
14105
  by_id = dict(primary.by_id)
14799
14106
  by_title = {key: list(value) for key, value in primary.by_title.items()}
@@ -14804,42 +14111,12 @@ def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
14804
14111
 
14805
14112
  for field_id, field in extra.by_id.items():
14806
14113
  if field_id in by_id:
14807
- merged_field = _merge_subtable_parent_field(by_id[field_id], field)
14808
- if merged_field is not by_id[field_id]:
14809
- by_id[field_id] = merged_field
14810
- _replace_field_in_lookup(by_title, merged_field)
14811
- _replace_field_in_lookup(by_alias, merged_field)
14812
14114
  continue
14813
14115
  by_id[field_id] = field
14814
14116
  by_title.setdefault(_normalize_field_lookup_key(field.que_title), []).append(field)
14815
14117
  for alias in field.aliases:
14816
14118
  by_alias.setdefault(_normalize_field_lookup_key(alias), []).append(field)
14817
14119
 
14818
- for field_id, fields in extra.subtable_leaf_by_id.items():
14819
- merged = subtable_leaf_by_id.setdefault(field_id, [])
14820
- existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
14821
- for field in fields:
14822
- key = (field.field.que_id, field.parent_field.que_id)
14823
- if key not in existing:
14824
- merged.append(field)
14825
- existing.add(key)
14826
- for title, fields in extra.subtable_leaf_by_title.items():
14827
- merged = subtable_leaf_by_title.setdefault(title, [])
14828
- existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
14829
- for field in fields:
14830
- key = (field.field.que_id, field.parent_field.que_id)
14831
- if key not in existing:
14832
- merged.append(field)
14833
- existing.add(key)
14834
- for alias, fields in extra.subtable_leaf_by_alias.items():
14835
- merged = subtable_leaf_by_alias.setdefault(alias, [])
14836
- existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
14837
- for field in fields:
14838
- key = (field.field.que_id, field.parent_field.que_id)
14839
- if key not in existing:
14840
- merged.append(field)
14841
- existing.add(key)
14842
-
14843
14120
  return FieldIndex(
14844
14121
  by_id=by_id,
14845
14122
  by_title=by_title,
@@ -16230,28 +15507,6 @@ def _record_detail_unavailable_context(section: str, message: str, exc: Qingflow
16230
15507
  "message": message,
16231
15508
  "category": exc.category,
16232
15509
  }
16233
- if is_auth_like_error(exc):
16234
- payload["auth_like"] = True
16235
- payload["error_code"] = "AUTH_REQUIRED"
16236
- if exc.backend_code is not None:
16237
- payload["backend_code"] = exc.backend_code
16238
- if exc.http_status is not None:
16239
- payload["http_status"] = exc.http_status
16240
- request_id = getattr(exc, "request_id", None)
16241
- if request_id:
16242
- payload["request_id"] = request_id
16243
- details = exc.details if isinstance(exc.details, dict) else {}
16244
- error_code = details.get("error_code")
16245
- if error_code and not payload.get("error_code"):
16246
- payload["error_code"] = error_code
16247
- return payload
16248
-
16249
-
16250
- def _record_detail_error_warning_fields(exc: QingflowApiError) -> JSONObject:
16251
- payload: JSONObject = {"category": exc.category}
16252
- if is_auth_like_error(exc):
16253
- payload["auth_like"] = True
16254
- payload["error_code"] = "AUTH_REQUIRED"
16255
15510
  if exc.backend_code is not None:
16256
15511
  payload["backend_code"] = exc.backend_code
16257
15512
  if exc.http_status is not None:
@@ -16261,35 +15516,11 @@ def _record_detail_error_warning_fields(exc: QingflowApiError) -> JSONObject:
16261
15516
  payload["request_id"] = request_id
16262
15517
  details = exc.details if isinstance(exc.details, dict) else {}
16263
15518
  error_code = details.get("error_code")
16264
- if error_code and not payload.get("error_code"):
15519
+ if error_code:
16265
15520
  payload["error_code"] = error_code
16266
15521
  return payload
16267
15522
 
16268
15523
 
16269
- def _record_detail_refreshed_source_url(refresh_result: Any) -> str | None:
16270
- if isinstance(refresh_result, dict):
16271
- return _normalize_optional_text(refresh_result.get("source_url"))
16272
- return _normalize_optional_text(refresh_result)
16273
-
16274
-
16275
- def _record_detail_append_refresh_warning(
16276
- warnings: list[JSONObject],
16277
- refresh_result: Any,
16278
- *,
16279
- id_key: str,
16280
- id_value: str,
16281
- ) -> None:
16282
- if not isinstance(refresh_result, dict):
16283
- return
16284
- warning = refresh_result.get("warning")
16285
- if not isinstance(warning, dict):
16286
- return
16287
- payload: JSONObject = dict(warning)
16288
- payload.setdefault("code", "ASSET_STORAGE_URL_REFRESH_FAILED")
16289
- payload.setdefault(id_key, id_value)
16290
- warnings.append(payload)
16291
-
16292
-
16293
15524
  _RECORD_MEDIA_IMG_SRC_RE = re.compile(r"""<img\b[^>]*\bsrc\s*=\s*["']?([^"'\s>]+)""", re.IGNORECASE)
16294
15525
  _RECORD_MEDIA_MD_IMAGE_RE = re.compile(r"""!\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)""")
16295
15526
  _RECORD_MEDIA_URL_RE = re.compile(r"""https?://[^\s<>"')\]]+""", re.IGNORECASE)
@@ -16444,14 +15675,7 @@ def _record_detail_media_assets_payload(
16444
15675
  except QingflowApiError as exc:
16445
15676
  blocked = exc.http_status in {401, 403}
16446
15677
  if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
16447
- refresh_result = refresh_source_url(candidate)
16448
- _record_detail_append_refresh_warning(
16449
- warnings,
16450
- refresh_result,
16451
- id_key="asset_id",
16452
- id_value=asset_id,
16453
- )
16454
- refreshed_url = _record_detail_refreshed_source_url(refresh_result)
15678
+ refreshed_url = _normalize_optional_text(refresh_source_url(candidate))
16455
15679
  if refreshed_url and refreshed_url != source_url:
16456
15680
  refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
16457
15681
  try:
@@ -16493,13 +15717,14 @@ def _record_detail_media_assets_payload(
16493
15717
  "readable_by_agent": False,
16494
15718
  }
16495
15719
  )
16496
- warning: JSONObject = {
16497
- "code": warning_code,
16498
- "asset_id": asset_id,
16499
- "message": f"record_get could not download image asset {asset_id}: {exc.message}",
16500
- }
16501
- warning.update(_record_detail_error_warning_fields(exc))
16502
- warnings.append(warning)
15720
+ warnings.append(
15721
+ {
15722
+ "code": warning_code,
15723
+ "asset_id": asset_id,
15724
+ "message": f"record_get could not download image asset {asset_id}: {exc.message}",
15725
+ "http_status": exc.http_status,
15726
+ }
15727
+ )
16503
15728
  continue
16504
15729
 
16505
15730
  if not isinstance(content, bytes):
@@ -16717,14 +15942,7 @@ def _record_detail_file_assets_payload(
16717
15942
  except QingflowApiError as exc:
16718
15943
  blocked = exc.http_status in {401, 403}
16719
15944
  if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
16720
- refresh_result = refresh_source_url(candidate)
16721
- _record_detail_append_refresh_warning(
16722
- warnings,
16723
- refresh_result,
16724
- id_key="file_asset_id",
16725
- id_value=file_asset_id,
16726
- )
16727
- refreshed_url = _record_detail_refreshed_source_url(refresh_result)
15945
+ refreshed_url = _normalize_optional_text(refresh_source_url(candidate))
16728
15946
  if refreshed_url and refreshed_url != source_url:
16729
15947
  refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
16730
15948
  try:
@@ -16771,13 +15989,14 @@ def _record_detail_file_assets_payload(
16771
15989
  "extraction": {"status": "failed", "text_path": None, "preview": None},
16772
15990
  }
16773
15991
  )
16774
- warning = {
16775
- "code": warning_code,
16776
- "file_asset_id": file_asset_id,
16777
- "message": f"record_get could not download file asset {file_asset_id}: {exc.message}",
16778
- }
16779
- warning.update(_record_detail_error_warning_fields(exc))
16780
- warnings.append(warning)
15992
+ warnings.append(
15993
+ {
15994
+ "code": warning_code,
15995
+ "file_asset_id": file_asset_id,
15996
+ "message": f"record_get could not download file asset {file_asset_id}: {exc.message}",
15997
+ "http_status": exc.http_status,
15998
+ }
15999
+ )
16781
16000
  continue
16782
16001
 
16783
16002
  if not isinstance(content, bytes):
@@ -18896,14 +18115,12 @@ def _extract_sort_selector(item: JSONObject) -> JSONValue:
18896
18115
  return None
18897
18116
 
18898
18117
 
18899
- def _normalize_public_column_selectors(columns: list[JSONObject | int | str]) -> list[int]:
18118
+ def _normalize_public_column_selectors(columns: list[JSONObject | int]) -> list[int]:
18900
18119
  normalized: list[int] = []
18901
18120
  for item in columns:
18902
18121
  field_id: int | None = None
18903
18122
  if isinstance(item, int):
18904
18123
  field_id = item
18905
- elif isinstance(item, str):
18906
- field_id = _coerce_count(item)
18907
18124
  elif isinstance(item, dict):
18908
18125
  _ensure_allowed_record_list_keys(
18909
18126
  item,
@@ -18915,21 +18132,19 @@ def _normalize_public_column_selectors(columns: list[JSONObject | int | str]) ->
18915
18132
  if field_id is None or field_id < 0:
18916
18133
  raise_tool_error(
18917
18134
  QingflowApiError.config_error(
18918
- "columns must be a list of field_id integers, integer strings, or {field_id} objects"
18135
+ "columns must be a list of field_id integers or {field_id} objects"
18919
18136
  )
18920
18137
  )
18921
18138
  normalized.append(field_id)
18922
18139
  return normalized
18923
18140
 
18924
18141
 
18925
- def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int | str]) -> list[int]:
18142
+ def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int]) -> list[int]:
18926
18143
  normalized: list[int] = []
18927
18144
  for item in query_fields:
18928
18145
  field_id: int | None = None
18929
18146
  if isinstance(item, int):
18930
18147
  field_id = item
18931
- elif isinstance(item, str):
18932
- field_id = _coerce_count(item)
18933
18148
  elif isinstance(item, dict):
18934
18149
  _ensure_allowed_record_list_keys(
18935
18150
  item,
@@ -18941,7 +18156,7 @@ def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int
18941
18156
  if field_id is None or field_id < 0:
18942
18157
  raise_tool_error(
18943
18158
  QingflowApiError.config_error(
18944
- "query_fields must be a list of field_id integers, integer strings, or {field_id} objects"
18159
+ "query_fields must be a list of field_id integers or {field_id} objects"
18945
18160
  )
18946
18161
  )
18947
18162
  normalized.append(field_id)