@josephyan/qingflow-cli 1.1.4 → 1.1.5

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 (154) hide show
  1. package/README.md +7 -3
  2. package/docs/local-agent-install.md +57 -6
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/bin/qingflow.mjs +1 -34
  6. package/npm/lib/runtime.mjs +21 -101
  7. package/npm/scripts/postinstall.mjs +1 -10
  8. package/package.json +3 -2
  9. package/pyproject.toml +1 -1
  10. package/skills/qingflow-cli/SKILL.md +58 -44
  11. package/skills/qingflow-cli/manifest.yaml +1 -1
  12. package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
  13. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
  14. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
  15. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
  16. package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
  17. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
  18. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
  19. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
  21. package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
  22. package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
  23. package/skills/qingflow-cli/reference/builder/README.md +41 -0
  24. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
  25. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
  26. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
  27. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
  28. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
  29. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
  30. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
  31. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
  32. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
  33. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
  34. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
  35. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
  36. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
  37. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
  38. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
  39. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
  40. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
  41. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
  42. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
  43. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
  44. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
  45. package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
  46. package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
  47. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
  48. package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
  49. package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
  50. package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
  51. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
  52. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
  53. package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
  54. package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
  55. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
  56. package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
  57. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
  58. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
  59. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
  60. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
  61. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
  62. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
  63. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
  64. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
  65. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
  66. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
  67. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
  68. package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
  69. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
  70. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
  71. package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
  72. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
  73. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
  74. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
  75. package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
  76. package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
  77. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
  78. package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
  79. package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
  80. package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
  81. package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
  82. package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
  83. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
  84. package/skills/qingflow-mcp-setup/SKILL.md +115 -0
  85. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  86. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  87. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  88. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  89. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  90. package/src/qingflow_mcp/__init__.py +1 -1
  91. package/src/qingflow_mcp/__main__.py +6 -2
  92. package/src/qingflow_mcp/builder_facade/models.py +282 -102
  93. package/src/qingflow_mcp/builder_facade/service.py +4166 -929
  94. package/src/qingflow_mcp/cli/commands/builder.py +316 -298
  95. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  96. package/src/qingflow_mcp/cli/commands/common.py +12 -3
  97. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  98. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  99. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  100. package/src/qingflow_mcp/cli/commands/record.py +101 -27
  101. package/src/qingflow_mcp/cli/commands/task.py +28 -47
  102. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  103. package/src/qingflow_mcp/cli/context.py +0 -3
  104. package/src/qingflow_mcp/cli/formatters.py +784 -16
  105. package/src/qingflow_mcp/cli/main.py +117 -33
  106. package/src/qingflow_mcp/errors.py +43 -2
  107. package/src/qingflow_mcp/public_surface.py +26 -17
  108. package/src/qingflow_mcp/response_trim.py +81 -17
  109. package/src/qingflow_mcp/server.py +14 -12
  110. package/src/qingflow_mcp/server_app_builder.py +65 -21
  111. package/src/qingflow_mcp/server_app_user.py +22 -16
  112. package/src/qingflow_mcp/session_store.py +11 -7
  113. package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
  114. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  115. package/src/qingflow_mcp/solution/executor.py +245 -18
  116. package/src/qingflow_mcp/tools/ai_builder_tools.py +1780 -406
  117. package/src/qingflow_mcp/tools/app_tools.py +184 -43
  118. package/src/qingflow_mcp/tools/approval_tools.py +197 -35
  119. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  120. package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
  121. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  122. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  123. package/src/qingflow_mcp/tools/export_tools.py +244 -34
  124. package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
  125. package/src/qingflow_mcp/tools/file_tools.py +9 -3
  126. package/src/qingflow_mcp/tools/import_tools.py +336 -49
  127. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  128. package/src/qingflow_mcp/tools/package_tools.py +118 -6
  129. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  130. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  131. package/src/qingflow_mcp/tools/record_tools.py +1141 -356
  132. package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
  133. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  134. package/src/qingflow_mcp/tools/solution_tools.py +59 -45
  135. package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
  136. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  137. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  138. package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
  139. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
  140. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
  141. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
  142. /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
  143. /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
  144. /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
  145. /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
  146. /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
  147. /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
  148. /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
  149. /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
  150. /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
  151. /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
  152. /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
  153. /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
  154. /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
@@ -14,7 +14,7 @@ from datetime import UTC, datetime, timedelta
14
14
  from decimal import Decimal, InvalidOperation
15
15
  from io import BytesIO
16
16
  from pathlib import Path
17
- from typing import Any, cast
17
+ from typing import Any, Callable, cast
18
18
  from urllib.parse import parse_qs, unquote, urlsplit
19
19
  from uuid import uuid4
20
20
  from xml.etree import ElementTree
@@ -22,7 +22,7 @@ from xml.etree import ElementTree
22
22
  from mcp.server.fastmcp import FastMCP
23
23
 
24
24
  from ..config import DEFAULT_PROFILE, DEFAULT_RECORD_LIST_TYPE, DEFAULT_USER_AGENT, get_mcp_home
25
- from ..errors import QingflowApiError, raise_tool_error
25
+ from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
26
26
  from ..id_utils import normalize_positive_id_int, stringify_backend_id
27
27
  from ..json_types import JSONObject, JSONScalar, JSONValue
28
28
  from ..list_type_labels import (
@@ -75,6 +75,22 @@ 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
+ }
78
94
  LOOKUP_RESOLUTION_MIN_SCORE = 0.92
79
95
  LOOKUP_RESOLUTION_MIN_MARGIN = 0.08
80
96
  LOOKUP_CONFIRMATION_CANDIDATE_LIMIT = 5
@@ -131,6 +147,27 @@ SCHEMA_LINKAGE_REFERENCE_SOURCE_MESSAGE = "updating this field may auto-fill or
131
147
  SCHEMA_LINKAGE_REFERENCE_TARGET_MESSAGE = "this field is usually filled from an upstream reference selection or default matching logic"
132
148
  SCHEMA_LINKAGE_REFERENCE_BOTH_MESSAGE = "this field participates in reference-driven auto-fill logic"
133
149
  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
134
171
 
135
172
 
136
173
  @dataclass(slots=True)
@@ -215,6 +252,13 @@ class AccessibleViewRoute:
215
252
  view_type: str | None = None
216
253
 
217
254
 
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
+
218
262
  @dataclass(slots=True)
219
263
  class RecordContextRouteProbe:
220
264
  route: AccessibleViewRoute
@@ -322,11 +366,12 @@ class RecordTools(ToolBase):
322
366
  """注册当前工具到 MCP 服务。"""
323
367
  @mcp.tool()
324
368
  def record_insert_schema_get(
369
+ profile: str = DEFAULT_PROFILE,
325
370
  app_key: str = "",
326
371
  output_profile: str = "normal",
327
372
  ) -> JSONObject:
328
373
  return self.record_insert_schema_get_public(
329
- profile=DEFAULT_PROFILE,
374
+ profile=profile,
330
375
  app_key=app_key,
331
376
  output_profile=output_profile,
332
377
  )
@@ -409,6 +454,7 @@ class RecordTools(ToolBase):
409
454
  where: list[JSONObject] | None = None,
410
455
  order_by: list[JSONObject] | None = None,
411
456
  page: int = 1,
457
+ page_size: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
412
458
  view_id: str | None = None,
413
459
  output_profile: str = "normal",
414
460
  ) -> JSONObject:
@@ -421,6 +467,7 @@ class RecordTools(ToolBase):
421
467
  where=where or [],
422
468
  order_by=order_by or [],
423
469
  page=page,
470
+ page_size=page_size,
424
471
  view_id=view_id,
425
472
  list_type=None,
426
473
  view_key=None,
@@ -436,6 +483,7 @@ class RecordTools(ToolBase):
436
483
  )
437
484
  )
438
485
  def record_access(
486
+ profile: str = DEFAULT_PROFILE,
439
487
  app_key: str = "",
440
488
  view_id: str = "",
441
489
  columns: list[JSONObject | int] | None = None,
@@ -443,7 +491,7 @@ class RecordTools(ToolBase):
443
491
  order_by: list[JSONObject] | None = None,
444
492
  ) -> JSONObject:
445
493
  return self.record_access(
446
- profile=DEFAULT_PROFILE,
494
+ profile=profile,
447
495
  app_key=app_key,
448
496
  view_id=view_id,
449
497
  columns=columns or [],
@@ -458,7 +506,6 @@ class RecordTools(ToolBase):
458
506
  record_id: str = "",
459
507
  columns: list[JSONObject | int] | None = None,
460
508
  view_id: str | None = None,
461
- workflow_node_id: int | None = None,
462
509
  output_profile: str = "detail_context",
463
510
  ) -> JSONObject:
464
511
  return self.record_get_public(
@@ -467,11 +514,11 @@ class RecordTools(ToolBase):
467
514
  record_id=record_id,
468
515
  columns=columns or [],
469
516
  view_id=view_id,
470
- workflow_node_id=workflow_node_id,
517
+ workflow_node_id=None,
471
518
  output_profile=output_profile,
472
519
  )
473
520
 
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.")
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.")
475
522
  def record_logs_get(
476
523
  profile: str = DEFAULT_PROFILE,
477
524
  app_key: str = "",
@@ -487,12 +534,13 @@ class RecordTools(ToolBase):
487
534
 
488
535
  @mcp.tool()
489
536
  def record_browse_schema_get(
537
+ profile: str = DEFAULT_PROFILE,
490
538
  app_key: str = "",
491
539
  view_id: str = "",
492
540
  output_profile: str = "normal",
493
541
  ) -> JSONObject:
494
542
  return self.record_browse_schema_get_public(
495
- profile=DEFAULT_PROFILE,
543
+ profile=profile,
496
544
  app_key=app_key,
497
545
  view_id=view_id,
498
546
  output_profile=output_profile,
@@ -500,14 +548,17 @@ class RecordTools(ToolBase):
500
548
 
501
549
  @mcp.tool()
502
550
  def record_update_schema_get(
551
+ profile: str = DEFAULT_PROFILE,
503
552
  app_key: str = "",
504
553
  record_id: str = "",
554
+ view_id: str | None = None,
505
555
  output_profile: str = "normal",
506
556
  ) -> JSONObject:
507
557
  return self.record_update_schema_get_public(
508
- profile=DEFAULT_PROFILE,
558
+ profile=profile,
509
559
  app_key=app_key,
510
560
  record_id=record_id,
561
+ view_id=view_id,
511
562
  output_profile=output_profile,
512
563
  )
513
564
 
@@ -520,13 +571,14 @@ class RecordTools(ToolBase):
520
571
  )
521
572
  )
522
573
  def record_insert(
574
+ profile: str = DEFAULT_PROFILE,
523
575
  app_key: str = "",
524
576
  items: list[JSONObject] | None = None,
525
577
  verify_write: bool = True,
526
578
  output_profile: str = "normal",
527
579
  ) -> JSONObject:
528
580
  return self.record_insert_public(
529
- profile=DEFAULT_PROFILE,
581
+ profile=profile,
530
582
  app_key=app_key,
531
583
  items=items,
532
584
  verify_write=verify_write,
@@ -537,25 +589,29 @@ class RecordTools(ToolBase):
537
589
  description=(
538
590
  "Update one Qingflow record using a field map. "
539
591
  "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. "
540
593
  "It first tries the data-manager direct route, then falls back to the frontend custom-view edit route when available. "
541
594
  "Use record_update_schema_get for diagnostics or complex field-scope inspection."
542
595
  )
543
596
  )
544
597
  def record_update(
598
+ profile: str = DEFAULT_PROFILE,
545
599
  app_key: str = "",
546
600
  record_id: str | None = None,
547
601
  fields: JSONObject | None = None,
548
602
  items: list[JSONObject] | None = None,
603
+ view_id: str | None = None,
549
604
  dry_run: bool = False,
550
605
  verify_write: bool = True,
551
606
  output_profile: str = "normal",
552
607
  ) -> JSONObject:
553
608
  return self.record_update_public(
554
- profile=DEFAULT_PROFILE,
609
+ profile=profile,
555
610
  app_key=app_key,
556
611
  record_id=record_id,
557
612
  fields=fields,
558
613
  items=items,
614
+ view_id=view_id,
559
615
  dry_run=dry_run,
560
616
  verify_write=verify_write,
561
617
  output_profile=output_profile,
@@ -564,20 +620,23 @@ class RecordTools(ToolBase):
564
620
  @mcp.tool(
565
621
  description=(
566
622
  "Delete Qingflow records by record_id or record_ids. "
567
- "This tool does not accept view selectors; resolve target record ids first, then delete by id."
623
+ "Pass view_id when deleting from a known system list route; custom view deletion is not supported by this backend route."
568
624
  )
569
625
  )
570
626
  def record_delete(
627
+ profile: str = DEFAULT_PROFILE,
571
628
  app_key: str = "",
572
629
  record_id: str | None = None,
573
630
  record_ids: list[str] | None = None,
631
+ view_id: str | None = None,
574
632
  output_profile: str = "normal",
575
633
  ) -> JSONObject:
576
634
  return self.record_delete_public(
577
- profile=DEFAULT_PROFILE,
635
+ profile=profile,
578
636
  app_key=app_key,
579
637
  record_id=record_id,
580
638
  record_ids=record_ids or [],
639
+ view_id=view_id,
581
640
  output_profile=output_profile,
582
641
  )
583
642
 
@@ -849,6 +908,7 @@ class RecordTools(ToolBase):
849
908
  profile: str = DEFAULT_PROFILE,
850
909
  app_key: str,
851
910
  record_id: Any,
911
+ view_id: str | None = None,
852
912
  output_profile: str = "normal",
853
913
  ) -> JSONObject:
854
914
  """执行记录相关逻辑。"""
@@ -860,21 +920,44 @@ class RecordTools(ToolBase):
860
920
  def runner(session_profile, context):
861
921
  request_route = self._request_route_payload(context)
862
922
  self._clear_record_schema_caches(profile=profile, app_key=app_key, clear_view_caches=True)
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
- )
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)
877
944
  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]
878
961
  probes = self._probe_candidate_record_contexts(
879
962
  context,
880
963
  app_key=app_key,
@@ -976,6 +1059,7 @@ class RecordTools(ToolBase):
976
1059
  output_profile=normalized_output_profile,
977
1060
  view_probe_summary=probe_summary,
978
1061
  ambiguous_fields=[],
1062
+ preferred_view_id=preferred_view_id,
979
1063
  )
980
1064
 
981
1065
  ambiguous_field_ids: set[int] = set()
@@ -1022,6 +1106,7 @@ class RecordTools(ToolBase):
1022
1106
  output_profile=normalized_output_profile,
1023
1107
  view_probe_summary=probe_summary,
1024
1108
  ambiguous_fields=ambiguous_fields,
1109
+ preferred_view_id=preferred_view_id,
1025
1110
  )
1026
1111
 
1027
1112
  response: JSONObject = {
@@ -1046,6 +1131,8 @@ class RecordTools(ToolBase):
1046
1131
  "message": "record_update will try data-manager direct edit first, then a matching custom-view edit route, then a unique current-user todo save-only route when the target fields are editable on that workflow node.",
1047
1132
  },
1048
1133
  }
1134
+ if preferred_view_id:
1135
+ response["preferred_view_id"] = preferred_view_id
1049
1136
  if normalized_output_profile == "verbose":
1050
1137
  response["view_probe_summary"] = probe_summary
1051
1138
  response["record_context_probe"] = probe_summary
@@ -1106,6 +1193,7 @@ class RecordTools(ToolBase):
1106
1193
  output_profile: str,
1107
1194
  view_probe_summary: list[JSONObject],
1108
1195
  ambiguous_fields: list[JSONObject],
1196
+ preferred_view_id: str | None = None,
1109
1197
  ) -> JSONObject:
1110
1198
  """执行内部辅助逻辑。"""
1111
1199
  response: JSONObject = {
@@ -1123,6 +1211,8 @@ class RecordTools(ToolBase):
1123
1211
  "payload_template": {},
1124
1212
  "recommended_next_actions": recommended_next_actions,
1125
1213
  }
1214
+ if preferred_view_id:
1215
+ response["preferred_view_id"] = preferred_view_id
1126
1216
  if output_profile == "verbose":
1127
1217
  response["view_probe_summary"] = view_probe_summary
1128
1218
  response["ambiguous_fields"] = ambiguous_fields
@@ -1440,24 +1530,58 @@ class RecordTools(ToolBase):
1440
1530
  )
1441
1531
  warnings: list[JSONObject] = []
1442
1532
  scope_source = "static_applicant_scope"
1443
- if runtime_lookup:
1444
- state = self._build_candidate_lookup_state(
1445
- profile,
1446
- context,
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,
1447
1579
  app_key=app_key,
1448
- record_id=record_id_int,
1580
+ record_id_text=record_id_text,
1449
1581
  workflow_node_id=workflow_node_id,
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
- }
1582
+ fields_present=bool(normalized_fields),
1583
+ keyword=keyword,
1584
+ scope_source=scope_source,
1461
1585
  )
1462
1586
  total = len(items)
1463
1587
  start = (page_num - 1) * page_size
@@ -1550,41 +1674,75 @@ class RecordTools(ToolBase):
1550
1674
  )
1551
1675
  warnings: list[JSONObject] = []
1552
1676
  scope_source = "static_applicant_scope"
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
- ):
1677
+ try:
1678
+ if runtime_lookup:
1573
1679
  state = self._build_candidate_lookup_state(
1574
1680
  profile,
1575
1681
  context,
1576
1682
  app_key=app_key,
1577
- record_id=None,
1578
- workflow_node_id=None,
1579
- fields={},
1683
+ record_id=record_id_int,
1684
+ workflow_node_id=workflow_node_id,
1685
+ fields=normalized_fields,
1580
1686
  )
1581
1687
  items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
1582
1688
  scope_source = "backend_runtime_scope"
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
- }
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,
1588
1746
  )
1589
1747
  total = len(items)
1590
1748
  start = (page_num - 1) * page_size
@@ -1646,6 +1804,21 @@ class RecordTools(ToolBase):
1646
1804
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
1647
1805
  if limit <= 0:
1648
1806
  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
+ )
1649
1822
  legacy_warnings = _detect_analyze_legacy_warnings(
1650
1823
  dimensions=dimensions,
1651
1824
  metrics=metrics,
@@ -1662,7 +1835,7 @@ class RecordTools(ToolBase):
1662
1835
  list_type=list_type,
1663
1836
  view_key=view_key,
1664
1837
  view_name=view_name,
1665
- allow_default=True,
1838
+ allow_default=False,
1666
1839
  )
1667
1840
  if not _view_type_supports_analysis(resolved_view.view_type):
1668
1841
  raise_tool_error(
@@ -1725,6 +1898,7 @@ class RecordTools(ToolBase):
1725
1898
  order_by: list[JSONObject],
1726
1899
  limit: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
1727
1900
  page: int = 1,
1901
+ page_size: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
1728
1902
  view_id: str | None = None,
1729
1903
  list_type: int | None = None,
1730
1904
  view_key: str | None = None,
@@ -1743,6 +1917,23 @@ class RecordTools(ToolBase):
1743
1917
  raise_tool_error(QingflowApiError.config_error("limit must be positive"))
1744
1918
  if page <= 0:
1745
1919
  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
+ )
1746
1937
  view_route, compatibility_warnings = self._resolve_accessible_view_route_for_public(
1747
1938
  profile=profile,
1748
1939
  app_key=app_key,
@@ -1750,7 +1941,7 @@ class RecordTools(ToolBase):
1750
1941
  list_type=list_type,
1751
1942
  view_key=view_key,
1752
1943
  view_name=view_name,
1753
- allow_default=True,
1944
+ allow_default=False,
1754
1945
  )
1755
1946
  if not _view_type_supports_analysis(view_route.view_type):
1756
1947
  raise_tool_error(
@@ -1800,12 +1991,12 @@ class RecordTools(ToolBase):
1800
1991
  app_key=app_key,
1801
1992
  view_route=view_route,
1802
1993
  page_num=page,
1803
- page_size=DEFAULT_LIST_PAGE_SIZE,
1994
+ page_size=page_size,
1804
1995
  query_key=normalized_query,
1805
1996
  search_que_ids=resolved_query_fields or None,
1806
1997
  match_rules=match_rules,
1807
1998
  sort_rules=sort_rules,
1808
- max_rows=limit,
1999
+ max_rows=min(limit, page_size),
1809
2000
  selected_fields=selected_fields,
1810
2001
  output_profile="verbose" if normalized_output_profile in {"verbose", "normalized"} else DEFAULT_OUTPUT_PROFILE,
1811
2002
  )
@@ -2224,6 +2415,7 @@ class RecordTools(ToolBase):
2224
2415
  requested_output_profile = _normalize_record_get_detail_output_profile(output_profile)
2225
2416
  record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
2226
2417
  normalized_columns = _normalize_public_column_selectors(columns)
2418
+ explicit_view_id = _normalize_optional_text(view_id)
2227
2419
 
2228
2420
  def runner(session_profile, context):
2229
2421
  resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
@@ -2249,17 +2441,45 @@ class RecordTools(ToolBase):
2249
2441
  "code": "OUTPUT_PROFILE_DEPRECATED_FOR_DETAIL_CONTEXT",
2250
2442
  "message": f"output_profile={requested_output_profile!r} is deprecated for record_get; detail_context is always returned.",
2251
2443
  })
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
- )
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
2263
2483
 
2264
2484
  return self._run_record_tool(profile, runner)
2265
2485
 
@@ -2274,6 +2494,16 @@ class RecordTools(ToolBase):
2274
2494
  ) -> JSONObject:
2275
2495
  """读取单条记录可见的全量数据日志和流程日志,写入本地 JSONL。"""
2276
2496
  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
+ )
2277
2507
 
2278
2508
  def runner(session_profile, context):
2279
2509
  resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
@@ -2284,21 +2514,45 @@ class RecordTools(ToolBase):
2284
2514
  list_type=None,
2285
2515
  view_key=None,
2286
2516
  view_name=None,
2287
- allow_default=True,
2517
+ allow_default=False,
2288
2518
  )
2289
2519
  warnings: list[JSONObject] = []
2290
2520
  warnings.extend(compatibility_warnings)
2291
2521
  warnings.extend(_view_filter_trust_warnings(resolved_view))
2292
2522
  unavailable_context: list[JSONObject] = []
2293
2523
 
2294
- schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
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
+ )
2295
2537
  index = _build_top_level_field_index(schema)
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
- )
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
+ )
2302
2556
  audit_context = _record_detail_audit_context(audit_info, workflow_node_id=None)
2303
2557
  detail_result, used_list_type, used_role = self._record_get_apply_detail(
2304
2558
  context,
@@ -2308,6 +2562,17 @@ class RecordTools(ToolBase):
2308
2562
  audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
2309
2563
  )
2310
2564
  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
+ )
2311
2576
  selected_fields = list(index.by_id.values())
2312
2577
  fields = [
2313
2578
  _record_detail_field_payload(field, _find_answer_for_field(cast(list[JSONValue], answer_list), field), focus_id_set=set())
@@ -2401,14 +2666,41 @@ class RecordTools(ToolBase):
2401
2666
  warnings: list[JSONObject],
2402
2667
  ) -> JSONObject:
2403
2668
  """执行内部辅助逻辑。"""
2404
- schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
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
+ )
2405
2685
  index = _build_top_level_field_index(schema)
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
- )
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
+ )
2412
2704
  audit_context = _record_detail_audit_context(audit_info, workflow_node_id=workflow_node_id)
2413
2705
  detail_result, used_list_type, used_role = self._record_get_apply_detail(
2414
2706
  context,
@@ -2418,13 +2710,24 @@ class RecordTools(ToolBase):
2418
2710
  audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
2419
2711
  )
2420
2712
  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
+ )
2421
2724
  selected_fields = list(index.by_id.values())
2422
2725
  row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id_int)
2423
2726
  normalized_record, _normalized_ambiguous_fields = _build_normalized_row_from_answers(
2424
2727
  cast(list[JSONValue], answer_list),
2425
2728
  selected_fields,
2426
2729
  )
2427
- if self._record_get_needs_schema_refresh(
2730
+ if schema_available and self._record_get_needs_schema_refresh(
2428
2731
  answer_list=cast(list[JSONValue], answer_list),
2429
2732
  selected_fields=selected_fields,
2430
2733
  record=row,
@@ -2440,7 +2743,6 @@ class RecordTools(ToolBase):
2440
2743
  index = _build_top_level_field_index(schema)
2441
2744
  selected_fields = list(index.by_id.values())
2442
2745
 
2443
- unavailable_context: list[JSONObject] = []
2444
2746
  dynamic_reference_answers, dynamic_reference_unavailable = self._record_get_dynamic_reference_answers(
2445
2747
  context,
2446
2748
  app_key=app_key,
@@ -2599,7 +2901,20 @@ class RecordTools(ToolBase):
2599
2901
  ) -> JSONObject:
2600
2902
  """执行内部辅助逻辑。"""
2601
2903
  if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
2602
- return self._get_view_form_schema(profile, context, resolved_view.view_selection.view_key, force_refresh=force_refresh)
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
+ )
2603
2918
  return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
2604
2919
 
2605
2920
  def _record_get_audit_info(
@@ -2660,7 +2975,7 @@ class RecordTools(ToolBase):
2660
2975
  )
2661
2976
  return result if isinstance(result, dict) else {"value": result}, list_type, role
2662
2977
  except QingflowApiError as exc:
2663
- if resolved_view.list_type is not None or exc.backend_code != 40002:
2978
+ if resolved_view.list_type is not None or not _is_record_permission_denied_error(exc):
2664
2979
  raise
2665
2980
  last_error: QingflowApiError = exc
2666
2981
  for fallback_list_type in (14, 1, 2, 12):
@@ -2678,7 +2993,7 @@ class RecordTools(ToolBase):
2678
2993
  return result if isinstance(result, dict) else {"value": result}, fallback_list_type, role
2679
2994
  except QingflowApiError as fallback_exc:
2680
2995
  last_error = fallback_exc
2681
- if fallback_exc.backend_code == 40002:
2996
+ if _is_record_permission_denied_error(fallback_exc):
2682
2997
  continue
2683
2998
  raise
2684
2999
  raise last_error
@@ -2776,6 +3091,8 @@ class RecordTools(ToolBase):
2776
3091
  if target_app_key == app_key and str(target_record_id) == str(source_record_id):
2777
3092
  reference_payload["self_reference"] = True
2778
3093
  except QingflowApiError as exc:
3094
+ if is_auth_like_error(exc):
3095
+ raise
2779
3096
  unavailable = _record_detail_unavailable_context(
2780
3097
  "reference_detail",
2781
3098
  f"引用字段「{field.que_title}」的目标记录详情获取失败。",
@@ -2873,6 +3190,8 @@ class RecordTools(ToolBase):
2873
3190
  json_body=body,
2874
3191
  )
2875
3192
  except QingflowApiError as exc:
3193
+ if is_auth_like_error(exc):
3194
+ raise
2876
3195
  unavailable = _record_detail_unavailable_context(
2877
3196
  "reference_runtime_match",
2878
3197
  "动态引用字段匹配数据获取失败。",
@@ -2927,6 +3246,8 @@ class RecordTools(ToolBase):
2927
3246
  },
2928
3247
  )
2929
3248
  except QingflowApiError as exc:
3249
+ if is_auth_like_error(exc):
3250
+ raise
2930
3251
  unavailable_context.append(_record_detail_unavailable_context("log_visibility", "侧边栏日志可见性获取失败。", exc))
2931
3252
  return {"status": "unavailable", "channel": channel, "data_log_visible": None, "workflow_log_visible": None}
2932
3253
  payload = visibility if isinstance(visibility, dict) else {}
@@ -2980,6 +3301,8 @@ class RecordTools(ToolBase):
2980
3301
  source="data_logs",
2981
3302
  )
2982
3303
  except QingflowApiError as exc:
3304
+ if is_auth_like_error(exc):
3305
+ raise
2983
3306
  unavailable_context.append(_record_detail_unavailable_context("data_logs", "最近数据日志获取失败。", exc))
2984
3307
  return _record_detail_log_unavailable_payload("data_logs", "fetch_unavailable")
2985
3308
 
@@ -3033,6 +3356,8 @@ class RecordTools(ToolBase):
3033
3356
  source="workflow_logs",
3034
3357
  )
3035
3358
  except QingflowApiError as exc:
3359
+ if is_auth_like_error(exc):
3360
+ raise
3036
3361
  unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "流程日志本次获取失败。", exc))
3037
3362
  return _record_detail_log_unavailable_payload("workflow_logs", "fetch_unavailable")
3038
3363
 
@@ -3076,6 +3401,8 @@ class RecordTools(ToolBase):
3076
3401
  deadline=deadline,
3077
3402
  )
3078
3403
  except QingflowApiError as exc:
3404
+ if is_auth_like_error(exc):
3405
+ raise
3079
3406
  unavailable_context.append(_record_detail_unavailable_context("data_logs", "全量数据日志获取失败。", exc))
3080
3407
  return _record_logs_unavailable_payload("data_logs", "fetch_unavailable")
3081
3408
 
@@ -3135,6 +3462,8 @@ class RecordTools(ToolBase):
3135
3462
  deadline=deadline,
3136
3463
  )
3137
3464
  except QingflowApiError as exc:
3465
+ if is_auth_like_error(exc):
3466
+ raise
3138
3467
  unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "全量流程日志获取失败。", exc))
3139
3468
  return _record_logs_unavailable_payload("workflow_logs", "fetch_unavailable")
3140
3469
 
@@ -3167,6 +3496,8 @@ class RecordTools(ToolBase):
3167
3496
  params["auditNodeId"] = audit_node_id
3168
3497
  payload = self.backend.request("GET", context, f"/app/{app_key}/asosChart", params=params)
3169
3498
  except QingflowApiError as exc:
3499
+ if is_auth_like_error(exc):
3500
+ raise
3170
3501
  unavailable_context.append(_record_detail_unavailable_context("associated_resources", "关联资源获取失败。", exc))
3171
3502
  return []
3172
3503
  return [_record_detail_associated_resource(item) for item in _record_detail_associated_resource_items(payload)]
@@ -3204,16 +3535,17 @@ class RecordTools(ToolBase):
3204
3535
  refresh_source_url=refresh_source_url,
3205
3536
  )
3206
3537
  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))
3207
3544
  return {
3208
3545
  "status": "unavailable",
3209
3546
  "local_dir": None,
3210
3547
  "items": [],
3211
- "warnings": [
3212
- {
3213
- "code": "MEDIA_ASSETS_UNAVAILABLE",
3214
- "message": f"record_get could not collect media assets: {exc}",
3215
- }
3216
- ],
3548
+ "warnings": [warning],
3217
3549
  }
3218
3550
 
3219
3551
  def _record_get_file_assets(
@@ -3251,16 +3583,17 @@ class RecordTools(ToolBase):
3251
3583
  refresh_source_url=refresh_source_url,
3252
3584
  )
3253
3585
  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))
3254
3592
  return {
3255
3593
  "status": "unavailable",
3256
3594
  "local_dir": None,
3257
3595
  "items": [],
3258
- "warnings": [
3259
- {
3260
- "code": "FILE_ASSETS_UNAVAILABLE",
3261
- "message": f"record_get could not collect file assets: {exc}",
3262
- }
3263
- ],
3596
+ "warnings": [warning],
3264
3597
  }
3265
3598
 
3266
3599
  def _record_get_refreshed_media_source_url(
@@ -3272,7 +3605,7 @@ class RecordTools(ToolBase):
3272
3605
  resolved_view: AccessibleViewRoute,
3273
3606
  audit_node_id: int | None,
3274
3607
  candidate: JSONObject,
3275
- ) -> str | None:
3608
+ ) -> JSONValue | None:
3276
3609
  """Refresh the detail payload once to recover an expired attachment storage signature."""
3277
3610
  if candidate.get("source") not in {"attachment", "image_field", "subtable"}:
3278
3611
  return None
@@ -3288,8 +3621,15 @@ class RecordTools(ToolBase):
3288
3621
  resolved_view=resolved_view,
3289
3622
  audit_node_id=audit_node_id,
3290
3623
  )
3291
- except QingflowApiError:
3292
- return None
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
+ }
3293
3633
  for answer in _record_detail_answers(detail_result):
3294
3634
  if not isinstance(answer, dict) or _coerce_count(answer.get("queId")) != field_id:
3295
3635
  continue
@@ -3325,6 +3665,7 @@ class RecordTools(ToolBase):
3325
3665
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
3326
3666
  if items is not None:
3327
3667
  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])
3328
3669
  return self._record_insert_public_batch(
3329
3670
  profile=profile,
3330
3671
  app_key=app_key,
@@ -3334,15 +3675,35 @@ class RecordTools(ToolBase):
3334
3675
  )
3335
3676
  if fields is not None and not isinstance(fields, dict):
3336
3677
  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])
3337
3680
  return self._record_insert_public_single(
3338
3681
  profile=profile,
3339
3682
  app_key=app_key,
3340
- fields=cast(JSONObject, fields or {}),
3683
+ fields=normalized_fields,
3341
3684
  verify_write=verify_write,
3342
3685
  output_profile=normalized_output_profile,
3343
3686
  capture_exceptions=False,
3344
3687
  )
3345
3688
 
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
+
3346
3707
  def _record_insert_public_single(
3347
3708
  self,
3348
3709
  *,
@@ -3676,7 +4037,11 @@ class RecordTools(ToolBase):
3676
4037
  "field_id": field_id,
3677
4038
  "error_code": error_code,
3678
4039
  "message": self._record_write_semantic_error_message(error_code, error.get("message")),
3679
- "next_action": self._record_write_next_action_for_error(error_code),
4040
+ "next_action": self._record_write_next_action_for_error(
4041
+ error_code,
4042
+ expected_format=expected_format,
4043
+ field_payload=field_payload,
4044
+ ),
3680
4045
  }
3681
4046
  if expected_format is not None:
3682
4047
  payload["expected_format"] = expected_format
@@ -3787,8 +4152,30 @@ class RecordTools(ToolBase):
3787
4152
  return _normalize_optional_text(fallback) or "字段值格式不正确。"
3788
4153
  return _normalize_optional_text(fallback) or "字段写入失败。"
3789
4154
 
3790
- def _record_write_next_action_for_error(self, error_code: str) -> str:
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:
3791
4162
  """执行内部辅助逻辑。"""
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,修正后只重试本行。"
3792
4179
  if error_code == "MISSING_REQUIRED_FIELD":
3793
4180
  return "补充该字段后只重试本行。"
3794
4181
  if error_code in {"FIELD_NOT_FOUND", "AMBIGUOUS_FIELD"}:
@@ -3804,6 +4191,7 @@ class RecordTools(ToolBase):
3804
4191
  record_id: Any | None,
3805
4192
  fields: JSONObject | None = None,
3806
4193
  items: list[JSONObject] | None = None,
4194
+ view_id: str | None = None,
3807
4195
  dry_run: bool = False,
3808
4196
  verify_write: bool = True,
3809
4197
  output_profile: str = "normal",
@@ -3824,6 +4212,7 @@ class RecordTools(ToolBase):
3824
4212
  profile=profile,
3825
4213
  app_key=app_key,
3826
4214
  items=normalized_items,
4215
+ view_id=view_id,
3827
4216
  dry_run=dry_run,
3828
4217
  verify_write=verify_write,
3829
4218
  output_profile=normalized_output_profile,
@@ -3840,6 +4229,7 @@ class RecordTools(ToolBase):
3840
4229
  app_key=app_key,
3841
4230
  record_id=record_id_int,
3842
4231
  fields=cast(JSONObject, fields or {}),
4232
+ view_id=view_id,
3843
4233
  verify_write=verify_write,
3844
4234
  output_profile=normalized_output_profile,
3845
4235
  )
@@ -3851,17 +4241,61 @@ class RecordTools(ToolBase):
3851
4241
  app_key: str,
3852
4242
  record_id: int,
3853
4243
  fields: JSONObject,
4244
+ view_id: str | None,
3854
4245
  verify_write: bool,
3855
4246
  output_profile: str,
4247
+ capture_exceptions: bool = False,
3856
4248
  ) -> JSONObject:
3857
4249
  """执行内部辅助逻辑。"""
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
- )
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)
3865
4299
  preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
3866
4300
  preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
3867
4301
  normalized_payload = self._record_write_normalized_payload(
@@ -3881,6 +4315,7 @@ class RecordTools(ToolBase):
3881
4315
  human_review=True,
3882
4316
  target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
3883
4317
  )
4318
+ write_attempted_ref(True)
3884
4319
  route_apply, tried_routes, route_blocker = self._record_update_apply_with_auto_route(
3885
4320
  profile=profile,
3886
4321
  app_key=app_key,
@@ -4161,7 +4596,9 @@ class RecordTools(ToolBase):
4161
4596
  )
4162
4597
 
4163
4598
  def _record_update_route_permission_denied(self, exc: QingflowApiError) -> bool:
4164
- if exc.backend_code in {40002, 40027, 40038, 404}:
4599
+ if is_auth_like_error(exc):
4600
+ return False
4601
+ if backend_code_int(exc) in {40002, 40027, 40038, 404}:
4165
4602
  return True
4166
4603
  if exc.http_status == 404:
4167
4604
  return True
@@ -4265,6 +4702,8 @@ class RecordTools(ToolBase):
4265
4702
  },
4266
4703
  )
4267
4704
  except QingflowApiError as exc:
4705
+ if not _is_optional_record_auxiliary_lookup_error(exc):
4706
+ raise
4268
4707
  return unavailable(
4269
4708
  error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
4270
4709
  reason="current-user todo task list is unavailable",
@@ -4312,6 +4751,8 @@ class RecordTools(ToolBase):
4312
4751
  f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
4313
4752
  )
4314
4753
  except QingflowApiError as exc:
4754
+ if not _is_optional_record_auxiliary_lookup_error(exc):
4755
+ raise
4315
4756
  return unavailable(
4316
4757
  error_code="TASK_EDITABLE_FIELDS_UNAVAILABLE",
4317
4758
  reason="workflow node editable field list is unavailable; record_update will not guess task editability",
@@ -4462,7 +4903,7 @@ class RecordTools(ToolBase):
4462
4903
  raise_tool_error(QingflowApiError.config_error("view_key is required for custom view update"))
4463
4904
 
4464
4905
  def runner(session_profile, context):
4465
- index = self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form) if verify_write else None
4906
+ index = self._get_view_field_index(profile, context, normalized_view_key, force_refresh=force_refresh_form) if verify_write else None
4466
4907
  normalized_answers = [dict(item) for item in answers if isinstance(item, dict)]
4467
4908
  self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
4468
4909
  result = self.backend.request(
@@ -4565,6 +5006,7 @@ class RecordTools(ToolBase):
4565
5006
  profile: str,
4566
5007
  app_key: str,
4567
5008
  items: list[JSONObject],
5009
+ view_id: str | None,
4568
5010
  dry_run: bool,
4569
5011
  verify_write: bool,
4570
5012
  output_profile: str,
@@ -4576,6 +5018,7 @@ class RecordTools(ToolBase):
4576
5018
  app_key=app_key,
4577
5019
  record_id=cast(int, item["record_id"]),
4578
5020
  fields=cast(JSONObject, item["fields"]),
5021
+ view_id=view_id,
4579
5022
  output_profile=output_profile,
4580
5023
  )
4581
5024
  for item in items
@@ -4604,8 +5047,10 @@ class RecordTools(ToolBase):
4604
5047
  app_key=app_key,
4605
5048
  record_id=record_id,
4606
5049
  fields=fields,
5050
+ view_id=view_id,
4607
5051
  verify_write=verify_write,
4608
5052
  output_profile=output_profile,
5053
+ capture_exceptions=True,
4609
5054
  )
4610
5055
  )
4611
5056
  except (QingflowApiError, RuntimeError) as exc:
@@ -4636,16 +5081,20 @@ class RecordTools(ToolBase):
4636
5081
  app_key: str,
4637
5082
  record_id: int,
4638
5083
  fields: JSONObject,
5084
+ view_id: str | None,
4639
5085
  output_profile: str,
4640
5086
  ) -> JSONObject:
4641
5087
  """执行内部辅助逻辑。"""
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
- )
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)
4649
5098
  preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
4650
5099
  normalized_payload = self._record_write_normalized_payload(
4651
5100
  operation="update",
@@ -4851,6 +5300,9 @@ class RecordTools(ToolBase):
4851
5300
  item: JSONObject = {
4852
5301
  "resource": data.get("resource"),
4853
5302
  "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"),
4854
5306
  "verification": data.get("verification"),
4855
5307
  "field_errors": cast(list[JSONObject], data.get("field_errors", [])),
4856
5308
  "confirmation_requests": cast(list[JSONObject], data.get("confirmation_requests", [])),
@@ -4860,7 +5312,7 @@ class RecordTools(ToolBase):
4860
5312
  if isinstance(update_route, dict):
4861
5313
  item["update_route"] = update_route
4862
5314
  tried_routes = response.get("tried_routes")
4863
- if isinstance(tried_routes, list):
5315
+ if isinstance(tried_routes, list) and (output_profile == "verbose" or response.get("status") != "success"):
4864
5316
  item["tried_routes"] = tried_routes
4865
5317
  blockers = data.get("blockers")
4866
5318
  if isinstance(blockers, list) and blockers:
@@ -4882,6 +5334,7 @@ class RecordTools(ToolBase):
4882
5334
  app_key: str,
4883
5335
  record_id: int,
4884
5336
  fields: JSONObject,
5337
+ preferred_view_id: str | None = None,
4885
5338
  force_refresh_form: bool,
4886
5339
  ) -> JSONObject:
4887
5340
  """执行内部辅助逻辑。"""
@@ -4889,6 +5342,25 @@ class RecordTools(ToolBase):
4889
5342
  request_route = self._request_route_payload(context)
4890
5343
  def build_once(*, effective_force_refresh: bool) -> JSONObject:
4891
5344
  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)
4892
5364
  probes = self._probe_candidate_record_contexts(
4893
5365
  context,
4894
5366
  app_key=app_key,
@@ -5102,41 +5574,24 @@ class RecordTools(ToolBase):
5102
5574
  "data": first_confirmation_plan,
5103
5575
  }
5104
5576
 
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
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
5127
5580
  return {
5128
5581
  "profile": profile,
5129
5582
  "ws_id": session_profile.selected_ws_id,
5130
5583
  "ok": True,
5131
5584
  "request_route": request_route,
5132
- "data": union_plan,
5585
+ "data": first_blocked_plan,
5133
5586
  }
5134
5587
 
5135
5588
  blocked_data = self._build_auto_view_blocked_preflight_data(
5136
5589
  app_key=app_key,
5137
5590
  record_id=record_id,
5138
5591
  blockers=["NO_SINGLE_VIEW_CAN_UPDATE_ALL_FIELDS"],
5139
- warnings=[],
5592
+ warnings=[
5593
+ "record_update requires one executable frontend route for the full payload; it does not merge writable fields across multiple views."
5594
+ ],
5140
5595
  recommended_next_actions=[
5141
5596
  "Call record_update_schema_get first to inspect the overall writable field set for this record.",
5142
5597
  "Reduce the update payload until all requested fields fit inside one matched accessible view.",
@@ -5183,6 +5638,7 @@ class RecordTools(ToolBase):
5183
5638
  union_writable_field_ids: set[int] = set()
5184
5639
  union_visible_question_ids: set[int] = set()
5185
5640
  matched_view_payloads: list[JSONObject] = []
5641
+ union_index: FieldIndex | None = None
5186
5642
 
5187
5643
  for candidate in matched_routes:
5188
5644
  browse_scope = self._build_browse_write_scope(
@@ -5192,11 +5648,13 @@ class RecordTools(ToolBase):
5192
5648
  candidate,
5193
5649
  force_refresh=force_refresh_form,
5194
5650
  )
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)
5195
5653
  union_writable_field_ids.update(cast(set[int], browse_scope["writable_field_ids"]))
5196
5654
  union_visible_question_ids.update(cast(set[int], browse_scope["visible_question_ids"]))
5197
5655
  matched_view_payloads.append(_accessible_view_payload(candidate))
5198
5656
 
5199
- if not union_writable_field_ids and not union_visible_question_ids:
5657
+ if union_index is None or (not union_writable_field_ids and not union_visible_question_ids):
5200
5658
  return None
5201
5659
 
5202
5660
  plan_data = self._build_record_write_preflight(
@@ -5213,10 +5671,9 @@ class RecordTools(ToolBase):
5213
5671
  view_key=None,
5214
5672
  view_name=None,
5215
5673
  existing_answers_override=current_answers,
5674
+ field_index_override=union_index,
5216
5675
  )
5217
5676
 
5218
- schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
5219
- app_index = _build_applicant_top_level_field_index(schema)
5220
5677
  validation = cast(JSONObject, plan_data.get("validation", {}))
5221
5678
  invalid_fields = cast(list[JSONObject], validation.get("invalid_fields", []))
5222
5679
  missing_required_fields = cast(list[JSONObject], validation.get("missing_required_fields", []))
@@ -5227,12 +5684,21 @@ class RecordTools(ToolBase):
5227
5684
  invalid_fields.extend(
5228
5685
  self._validate_view_scoped_subtable_answers(
5229
5686
  normalized_answers=cast(list[JSONObject], plan_data.get("normalized_answers", [])),
5230
- full_index=app_index,
5231
- selector_index=app_index,
5687
+ full_index=union_index,
5688
+ selector_index=union_index,
5232
5689
  visible_question_ids=union_visible_question_ids,
5233
5690
  )
5234
5691
  )
5235
5692
 
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
+ ]
5236
5702
  existing_readonly_ids = {
5237
5703
  str(_coerce_count(item.get("que_id")))
5238
5704
  for item in readonly_or_system_fields
@@ -5396,7 +5862,13 @@ class RecordTools(ToolBase):
5396
5862
  view_type=None,
5397
5863
  )
5398
5864
  )
5399
- for item in self._get_view_list(profile, context, app_key):
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:
5400
5872
  if not isinstance(item, dict):
5401
5873
  continue
5402
5874
  view_key = _normalize_optional_text(item.get("viewKey"))
@@ -5453,7 +5925,9 @@ class RecordTools(ToolBase):
5453
5925
  return payload
5454
5926
 
5455
5927
  def _is_record_context_route_miss(self, error: QingflowApiError) -> bool:
5456
- if error.backend_code in {40002, 40023, 40027, 40038, 404}:
5928
+ if is_auth_like_error(error):
5929
+ return False
5930
+ if backend_code_int(error) in {40002, 40023, 40027, 40038, 404}:
5457
5931
  return True
5458
5932
  if error.http_status == 404:
5459
5933
  return True
@@ -5483,11 +5957,12 @@ class RecordTools(ToolBase):
5483
5957
  used_list_type = None
5484
5958
  else:
5485
5959
  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)
5486
5961
  record = self.backend.request(
5487
5962
  "GET",
5488
5963
  context,
5489
5964
  f"/app/{app_key}/apply/{apply_id}",
5490
- params={"role": 1, "listType": used_list_type},
5965
+ params={"role": role, "listType": used_list_type},
5491
5966
  )
5492
5967
  answers = record.get("answers") if isinstance(record, dict) else None
5493
5968
  normalized_answers = [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []
@@ -5517,6 +5992,8 @@ class RecordTools(ToolBase):
5517
5992
  error_payload=None,
5518
5993
  )
5519
5994
  except QingflowApiError as exc:
5995
+ if not self._is_record_context_route_miss(exc):
5996
+ raise
5520
5997
  return RecordContextRouteProbe(
5521
5998
  route=resolved_view,
5522
5999
  answer_list=None,
@@ -5588,7 +6065,7 @@ class RecordTools(ToolBase):
5588
6065
  ]
5589
6066
 
5590
6067
  def _looks_like_generic_record_update_backend_failure(self, exc: QingflowApiError) -> bool:
5591
- if exc.backend_code == 500:
6068
+ if backend_code_int(exc) == 500:
5592
6069
  return True
5593
6070
  if exc.http_status is not None and exc.http_status >= 500:
5594
6071
  return True
@@ -5713,12 +6190,15 @@ class RecordTools(ToolBase):
5713
6190
  app_key: str,
5714
6191
  record_id: Any | None = None,
5715
6192
  record_ids: list[Any] | None = None,
6193
+ view_id: str | None = None,
6194
+ list_type: int | None = None,
5716
6195
  output_profile: str = "normal",
5717
6196
  ) -> JSONObject:
5718
6197
  """执行记录相关逻辑。"""
5719
6198
  normalized_output_profile = self._normalize_public_output_profile(output_profile)
5720
6199
  if not app_key:
5721
6200
  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)
5722
6202
  normalized_record_ids: list[int] = []
5723
6203
  for index, item in enumerate(record_ids or []):
5724
6204
  normalized_record_ids.append(normalize_positive_id_int(item, field_name=f"record_ids[{index}]"))
@@ -5738,21 +6218,72 @@ class RecordTools(ToolBase):
5738
6218
  "record_ids": [stringify_backend_id(item) for item in delete_ids],
5739
6219
  "answers": [],
5740
6220
  "submit_type": 1,
6221
+ "selection": {"view_id": view_id, "list_type": delete_list_type},
5741
6222
  }
5742
6223
  return self._record_delete_public_batch(
5743
6224
  profile=profile,
5744
6225
  app_key=app_key,
5745
6226
  delete_ids=delete_ids,
6227
+ list_type=delete_list_type,
5746
6228
  normalized_payload=normalized_payload,
5747
6229
  output_profile=normalized_output_profile,
5748
6230
  )
5749
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
+ )
6278
+ )
6279
+
5750
6280
  def _record_delete_public_batch(
5751
6281
  self,
5752
6282
  *,
5753
6283
  profile: str,
5754
6284
  app_key: str,
5755
6285
  delete_ids: list[int],
6286
+ list_type: int,
5756
6287
  normalized_payload: JSONObject,
5757
6288
  output_profile: str,
5758
6289
  ) -> JSONObject:
@@ -5762,7 +6293,7 @@ class RecordTools(ToolBase):
5762
6293
  for index, delete_id in enumerate(delete_ids):
5763
6294
  record_id_text = stringify_backend_id(delete_id)
5764
6295
  try:
5765
- raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=[delete_id])
6296
+ raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=[delete_id], list_type=list_type)
5766
6297
  request_route = cast(JSONObject, raw_apply.get("request_route")) if isinstance(raw_apply.get("request_route"), dict) else request_route
5767
6298
  ws_id = raw_apply.get("ws_id", ws_id)
5768
6299
  single_payload = {
@@ -5771,6 +6302,7 @@ class RecordTools(ToolBase):
5771
6302
  "record_ids": [record_id_text],
5772
6303
  "answers": [],
5773
6304
  "submit_type": 1,
6305
+ "selection": normalized_payload.get("selection"),
5774
6306
  }
5775
6307
  single_response = self._record_write_apply_response(
5776
6308
  raw_apply,
@@ -6053,12 +6585,13 @@ class RecordTools(ToolBase):
6053
6585
  preflight=raw_preflight,
6054
6586
  )
6055
6587
 
6056
- if uses_view_scope:
6588
+ if view_key is not None or view_name is not None:
6057
6589
  raise_tool_error(
6058
6590
  QingflowApiError.config_error(
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"
6591
+ "delete does not support custom view selectors; use a system view_id/list_type or resolve target record_ids first"
6060
6592
  )
6061
6593
  )
6594
+ delete_list_type = self._resolve_record_delete_list_type(view_id=view_id, list_type=list_type)
6062
6595
  if normalized_values or normalized_set:
6063
6596
  raise_tool_error(QingflowApiError.config_error("delete must not include values or set"))
6064
6597
  delete_ids = normalized_record_ids or ([record_id] if record_id is not None and record_id > 0 else [])
@@ -6070,8 +6603,9 @@ class RecordTools(ToolBase):
6070
6603
  "record_ids": delete_ids,
6071
6604
  "answers": [],
6072
6605
  "submit_type": submit_type_value,
6606
+ "selection": {"view_id": view_id, "list_type": delete_list_type},
6073
6607
  }
6074
- raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=delete_ids)
6608
+ raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=delete_ids, list_type=delete_list_type)
6075
6609
  return self._record_write_apply_response(
6076
6610
  raw_apply,
6077
6611
  operation="delete",
@@ -6221,7 +6755,9 @@ class RecordTools(ToolBase):
6221
6755
  or _normalize_optional_text(payload.get("appName"))
6222
6756
  or _normalize_optional_text(payload.get("appTitle"))
6223
6757
  )
6224
- except QingflowApiError:
6758
+ except QingflowApiError as exc:
6759
+ if is_auth_like_error(exc):
6760
+ raise
6225
6761
  name = None
6226
6762
  self._app_name_cache[cache_key] = name
6227
6763
  return name
@@ -6375,7 +6911,9 @@ class RecordTools(ToolBase):
6375
6911
  try:
6376
6912
  result = self.backend.request("GET", context, "/data/baseInfo", params={"queId": field.que_id})
6377
6913
  payload = result if isinstance(result, dict) else None
6378
- except QingflowApiError:
6914
+ except QingflowApiError as exc:
6915
+ if is_auth_like_error(exc):
6916
+ raise
6379
6917
  payload = None
6380
6918
  self._relation_base_info_cache[cache_key] = payload or {}
6381
6919
  return payload
@@ -6648,6 +7186,26 @@ class RecordTools(ToolBase):
6648
7186
  or bool(fields)
6649
7187
  )
6650
7188
 
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
+
6651
7209
  def _build_candidate_lookup_state(
6652
7210
  self,
6653
7211
  profile: str,
@@ -6666,7 +7224,9 @@ class RecordTools(ToolBase):
6666
7224
  if apply_id is not None:
6667
7225
  try:
6668
7226
  base_answers = self._load_record_answers_for_preflight(context, app_key=app_key, apply_id=apply_id)
6669
- except QingflowApiError:
7227
+ except QingflowApiError as exc:
7228
+ if not _is_optional_record_auxiliary_lookup_error(exc):
7229
+ raise
6670
7230
  context_complete = False
6671
7231
  state = LookupResolutionState(
6672
7232
  operation="update" if apply_id is not None else "insert",
@@ -7156,15 +7716,16 @@ class RecordTools(ToolBase):
7156
7716
  )
7157
7717
  if configured_candidate is not None:
7158
7718
  self._merge_department_candidate(merged, configured_candidate)
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)
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)
7168
7729
  filtered = _filter_department_candidates(list(merged.values()), keyword)
7169
7730
  filtered.sort(key=lambda item: (_normalize_optional_text(item.get("value")) or "", _coerce_count(item.get("id")) or 0))
7170
7731
  return filtered
@@ -8305,22 +8866,10 @@ class RecordTools(ToolBase):
8305
8866
  field_index_override: FieldIndex | None = None,
8306
8867
  ) -> JSONObject:
8307
8868
  """执行内部辅助逻辑。"""
8308
- schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
8309
- base_index = field_index_override or _build_applicant_top_level_field_index(schema)
8310
- question_relations = _collect_question_relations(schema)
8311
- runtime_linked_field_ids = _collect_linked_required_field_ids(question_relations)
8312
- runtime_linked_field_ids.update(_collect_option_linked_field_ids(base_index))
8313
- index = base_index
8314
- if operation == "create" and field_index_override is None:
8315
- linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
8316
- schema,
8317
- linked_field_ids=runtime_linked_field_ids,
8318
- )
8319
- index = _merge_field_indexes(base_index, linked_hidden_index)
8320
8869
  normalized_fields = fields or {}
8321
8870
  normalized_answers_input = answers or []
8322
8871
  resolved_view: AccessibleViewRoute | None = None
8323
- selector_index = index
8872
+ selector_index: FieldIndex | None = field_index_override
8324
8873
  browse_writable_field_ids: set[int] = set()
8325
8874
  visible_question_ids: set[int] = set()
8326
8875
  if any(item is not None for item in (view_id, list_type, view_key, view_name)):
@@ -8346,6 +8895,31 @@ class RecordTools(ToolBase):
8346
8895
  visible_question_ids = cast(set[int], browse_scope["visible_question_ids"])
8347
8896
  else:
8348
8897
  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
8349
8923
  resolved_fields = self._collect_write_plan_field_refs(fields=normalized_fields, answers=normalized_answers_input, index=selector_index)
8350
8924
  support_matrix = _summarize_write_support(resolved_fields)
8351
8925
  invalid_fields: list[JSONObject] = []
@@ -8389,7 +8963,9 @@ class RecordTools(ToolBase):
8389
8963
  apply_id=apply_id,
8390
8964
  )
8391
8965
  existing_answers_loaded = True
8392
- except QingflowApiError:
8966
+ except QingflowApiError as exc:
8967
+ if not _is_optional_record_auxiliary_lookup_error(exc):
8968
+ raise
8393
8969
  validation_warnings.append(
8394
8970
  "update preflight could not load the current record; required-field completeness and dynamic lookup context were not fully revalidated."
8395
8971
  )
@@ -8978,7 +9554,7 @@ class RecordTools(ToolBase):
8978
9554
  break
8979
9555
  except QingflowApiError as exc:
8980
9556
  last_error = exc
8981
- if exc.backend_code == 40002:
9557
+ if _is_record_permission_denied_error(exc):
8982
9558
  continue
8983
9559
  raise
8984
9560
  if result is None:
@@ -9081,7 +9657,21 @@ class RecordTools(ToolBase):
9081
9657
  normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
9082
9658
 
9083
9659
  def runner(session_profile, context):
9084
- index = self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form) if verify_write else None
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
9085
9675
  normalized_answers = self._resolve_answers(
9086
9676
  profile,
9087
9677
  context,
@@ -9089,6 +9679,7 @@ class RecordTools(ToolBase):
9089
9679
  answers=answers or [],
9090
9680
  fields=fields or {},
9091
9681
  force_refresh_form=force_refresh_form,
9682
+ field_index_override=update_index,
9092
9683
  )
9093
9684
  self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
9094
9685
  try:
@@ -9142,13 +9733,14 @@ class RecordTools(ToolBase):
9142
9733
  def record_delete(self, *, profile: str, app_key: str, apply_id: int, list_type: int) -> JSONObject:
9143
9734
  """执行记录相关逻辑。"""
9144
9735
  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)
9145
9737
 
9146
9738
  def runner(session_profile, context):
9147
9739
  result = self.backend.request(
9148
9740
  "DELETE",
9149
9741
  context,
9150
9742
  f"/app/{app_key}/apply",
9151
- json_body={"type": list_type, "applyIds": [normalized_apply_id]},
9743
+ json_body={"type": delete_list_type, "applyIds": [normalized_apply_id]},
9152
9744
  )
9153
9745
  return self._attach_human_review_notice(
9154
9746
  {
@@ -9157,6 +9749,7 @@ class RecordTools(ToolBase):
9157
9749
  "request_route": self._request_route_payload(context),
9158
9750
  "app_key": app_key,
9159
9751
  "apply_id": normalized_apply_id,
9752
+ "list_type": delete_list_type,
9160
9753
  "result": result,
9161
9754
  },
9162
9755
  operation="delete",
@@ -9201,7 +9794,7 @@ class RecordTools(ToolBase):
9201
9794
  "GET",
9202
9795
  context,
9203
9796
  f"/app/{app_key}/apply/{apply_id}",
9204
- params={"role": 1, "listType": list_type},
9797
+ params={"role": _record_detail_role_for_list_type(list_type), "listType": list_type},
9205
9798
  )
9206
9799
  answers = result.get("answers") if isinstance(result, dict) else None
9207
9800
  answer_list = answers if isinstance(answers, list) else []
@@ -9560,7 +10153,7 @@ class RecordTools(ToolBase):
9560
10153
  used_list_type: int | None = None
9561
10154
  if view_selection is not None:
9562
10155
  fallback_list_types = [view_route.list_type if view_route.list_type is not None else DEFAULT_RECORD_LIST_TYPE]
9563
- elif view_route.list_type is not None and view_route.list_type != DEFAULT_RECORD_LIST_TYPE:
10156
+ elif view_route.list_type is not None:
9564
10157
  fallback_list_types = [view_route.list_type]
9565
10158
  else:
9566
10159
  fallback_list_types = [DEFAULT_RECORD_LIST_TYPE, 14, 1, 2, 12]
@@ -9791,7 +10384,7 @@ class RecordTools(ToolBase):
9791
10384
  try:
9792
10385
  payload = self.backend.request("GET", context, f"/view/{view_key}/viewConfig")
9793
10386
  except QingflowApiError as exc:
9794
- if exc.backend_code in {40002, 40027, 404} or exc.http_status == 404:
10387
+ if _is_optional_schema_permission_error(exc):
9795
10388
  self._view_config_cache[cache_key] = None
9796
10389
  return None
9797
10390
  raise
@@ -9912,7 +10505,12 @@ class RecordTools(ToolBase):
9912
10505
  )
9913
10506
  normalized = _normalize_data_list_base_info_schema(payload)
9914
10507
  if not isinstance(normalized.get("formQues"), list) or not normalized.get("formQues"):
9915
- return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
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
9916
10514
  self._form_cache[cache_key] = normalized
9917
10515
  return normalized
9918
10516
 
@@ -9944,8 +10542,16 @@ class RecordTools(ToolBase):
9944
10542
  cache_key = (profile, f"view:{view_key}", "browse_view_base_info", None)
9945
10543
  if not force_refresh and cache_key in self._form_cache:
9946
10544
  return self._form_cache[cache_key]
9947
- payload = self.backend.request("GET", context, f"/view/{view_key}/apply/baseInfo")
9948
- normalized = _normalize_data_list_base_info_schema(payload)
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)
9949
10555
  self._form_cache[cache_key] = normalized
9950
10556
  return normalized
9951
10557
 
@@ -10001,22 +10607,6 @@ class RecordTools(ToolBase):
10001
10607
  force_refresh: bool,
10002
10608
  ) -> JSONObject:
10003
10609
  """Build the UI/table-view readable field scope from apply/baseInfo."""
10004
- applicant_index: FieldIndex | None
10005
- applicant_writable_field_ids: set[int]
10006
- try:
10007
- applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
10008
- except QingflowApiError as exc:
10009
- if exc.backend_code != 40002:
10010
- raise
10011
- applicant_index = None
10012
- applicant_writable_field_ids = set()
10013
- else:
10014
- applicant_writable_field_ids = {
10015
- field.que_id
10016
- for field in applicant_index.by_id.values()
10017
- if bool(self._schema_write_hints(field)["writable"])
10018
- }
10019
-
10020
10610
  if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
10021
10611
  schema = self._get_custom_view_browse_schema(
10022
10612
  profile,
@@ -10025,6 +10615,16 @@ class RecordTools(ToolBase):
10025
10615
  force_refresh=force_refresh,
10026
10616
  )
10027
10617
  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
+ }
10028
10628
  elif resolved_view is not None and resolved_view.kind == "system" and resolved_view.list_type is not None:
10029
10629
  schema = self._get_system_browse_base_info_schema(
10030
10630
  profile,
@@ -10034,34 +10634,26 @@ class RecordTools(ToolBase):
10034
10634
  force_refresh=force_refresh,
10035
10635
  )
10036
10636
  index = _build_top_level_field_index(schema)
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
- )
10041
-
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]]})
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
+ }
10048
10647
 
10049
- visible_question_ids = {field.que_id for field in index.by_id.values()}
10050
- if applicant_index is None:
10051
- writable_field_ids = {
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": {
10052
10653
  field.que_id
10053
- for field in index.by_id.values()
10654
+ for field in applicant_index.by_id.values()
10054
10655
  if bool(self._schema_write_hints(field)["writable"])
10055
- }
10056
- else:
10057
- writable_field_ids = {
10058
- field_id
10059
- for field_id in visible_question_ids
10060
- if field_id in applicant_writable_field_ids
10061
- }
10062
- return {
10063
- "index": index,
10064
- "writable_field_ids": writable_field_ids,
10656
+ },
10065
10657
  "visible_question_ids": visible_question_ids,
10066
10658
  }
10067
10659
 
@@ -10075,23 +10667,13 @@ class RecordTools(ToolBase):
10075
10667
  force_refresh: bool,
10076
10668
  ) -> JSONObject:
10077
10669
  """执行内部辅助逻辑。"""
10078
- applicant_index: FieldIndex | None
10079
- applicant_writable_field_ids: set[int]
10080
- try:
10081
- applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
10082
- except QingflowApiError as exc:
10083
- if exc.backend_code != 40002:
10084
- raise
10085
- applicant_index = None
10086
- applicant_writable_field_ids = set()
10087
- else:
10088
- applicant_writable_field_ids = {
10089
- field.que_id
10090
- for field in applicant_index.by_id.values()
10091
- if bool(self._schema_write_hints(field)["writable"])
10092
- }
10093
10670
  if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
10094
- schema = self._get_view_form_schema(profile, context, resolved_view.view_selection.view_key, force_refresh=force_refresh)
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
+ )
10095
10677
  index = _build_top_level_field_index(schema)
10096
10678
  visible_question_ids = self._get_view_question_ids(profile, context, resolved_view.view_selection.view_key)
10097
10679
  if not visible_question_ids:
@@ -10107,6 +10689,12 @@ class RecordTools(ToolBase):
10107
10689
  index = _build_top_level_field_index(schema)
10108
10690
  visible_question_ids = _question_ids_from_schema(schema)
10109
10691
  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
+ }
10110
10698
  index = applicant_index or _build_top_level_field_index(
10111
10699
  self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
10112
10700
  )
@@ -10125,43 +10713,13 @@ class RecordTools(ToolBase):
10125
10713
  "visible_question_ids": set(visible_question_ids),
10126
10714
  }
10127
10715
 
10128
- if applicant_index is None:
10129
- return {
10130
- "index": index,
10131
- "writable_field_ids": {
10132
- field.que_id
10133
- for field in index.by_id.values()
10134
- if bool(self._schema_write_hints(field)["writable"])
10135
- },
10136
- "visible_question_ids": visible_question_ids,
10137
- }
10138
-
10139
- augmented_fields = [
10140
- _clone_form_field(applicant_index.by_id.get(str(field.que_id)) or field)
10141
- for field in index.by_id.values()
10142
- ]
10143
- augmented_field_ids = {field.que_id for field in augmented_fields}
10144
- writable_field_ids = {
10145
- field_id
10146
- for field_id in visible_question_ids
10147
- if field_id in applicant_writable_field_ids
10148
- }
10149
- for field in applicant_index.by_id.values():
10150
- descendant_ids = _subtable_descendant_ids(field)
10151
- field_visible = field.que_id in visible_question_ids
10152
- descendant_visible = bool(descendant_ids and (descendant_ids & visible_question_ids))
10153
- if not field_visible and not descendant_visible:
10154
- continue
10155
- if field.que_id not in augmented_field_ids:
10156
- augmented_fields.append(_clone_form_field(field))
10157
- augmented_field_ids.add(field.que_id)
10158
- if descendant_visible:
10159
- visible_question_ids.add(field.que_id)
10160
- if field.que_id in applicant_writable_field_ids and (field_visible or descendant_visible):
10161
- writable_field_ids.add(field.que_id)
10162
10716
  return {
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,
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
+ },
10165
10723
  "visible_question_ids": visible_question_ids,
10166
10724
  }
10167
10725
 
@@ -10226,7 +10784,7 @@ class RecordTools(ToolBase):
10226
10784
  try:
10227
10785
  payload = self.backend.request("GET", context, f"/view/{view_key}/question")
10228
10786
  except QingflowApiError as exc:
10229
- if exc.backend_code in {40002, 40027}:
10787
+ if _is_record_permission_denied_error(exc):
10230
10788
  return set()
10231
10789
  raise
10232
10790
  if not isinstance(payload, list):
@@ -10270,7 +10828,7 @@ class RecordTools(ToolBase):
10270
10828
  )
10271
10829
  return True
10272
10830
  except QingflowApiError as exc:
10273
- if exc.backend_code in {40002, 40027}:
10831
+ if _is_record_permission_denied_error(exc):
10274
10832
  return False
10275
10833
  raise
10276
10834
 
@@ -10423,7 +10981,12 @@ class RecordTools(ToolBase):
10423
10981
  requested_name = _normalize_optional_text(view_name)
10424
10982
  if requested_key is None and requested_name is None:
10425
10983
  return None
10426
- views = self._get_view_list(profile, context, app_key)
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 = []
10427
10990
  selected: JSONObject | None = None
10428
10991
  if requested_key is not None:
10429
10992
  selected = next((item for item in views if _normalize_optional_text(item.get("viewKey")) == requested_key), None)
@@ -10621,9 +11184,11 @@ class RecordTools(ToolBase):
10621
11184
 
10622
11185
  def _should_retry_list_type_fallback(self, error: QingflowApiError) -> bool:
10623
11186
  """执行内部辅助逻辑。"""
10624
- if error.backend_code in {40002, 40027, 404}:
11187
+ if is_auth_like_error(error):
11188
+ return False
11189
+ if backend_code_int(error) in {40002, 40027, 404}:
10625
11190
  return True
10626
- if error.http_status in {404, 500}:
11191
+ if error.http_status == 404:
10627
11192
  return True
10628
11193
  return False
10629
11194
 
@@ -11405,6 +11970,8 @@ class RecordTools(ToolBase):
11405
11970
  schema: JSONObject = {}
11406
11971
  if isinstance(raw.get("subQuestions"), list):
11407
11972
  schema["formQues"] = [raw["subQuestions"]]
11973
+ elif isinstance(raw.get("subQues"), list):
11974
+ schema["formQues"] = [raw["subQues"]]
11408
11975
  elif isinstance(raw.get("innerQuestions"), list):
11409
11976
  schema["formQues"] = raw["innerQuestions"]
11410
11977
  index = _build_field_index(schema)
@@ -11444,6 +12011,70 @@ class RecordTools(ToolBase):
11444
12011
  )
11445
12012
  )
11446
12013
 
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
+
11447
12078
  def _request_route_payload(self, context) -> JSONObject: # type: ignore[no-untyped-def]
11448
12079
  """执行内部辅助逻辑。"""
11449
12080
  describe_route = getattr(self.backend, "describe_route", None)
@@ -11617,7 +12248,7 @@ class RecordTools(ToolBase):
11617
12248
  selection: JSONObject | None,
11618
12249
  ) -> None:
11619
12250
  """执行内部辅助逻辑。"""
11620
- if exc.backend_code != 40002:
12251
+ if not _is_record_permission_denied_error(exc):
11621
12252
  raise exc
11622
12253
  raise_tool_error(
11623
12254
  QingflowApiError(
@@ -11783,6 +12414,7 @@ class RecordTools(ToolBase):
11783
12414
  response_status = raw_status or "failed"
11784
12415
  update_route = raw_apply.get("update_route") if isinstance(raw_apply.get("update_route"), dict) else None
11785
12416
  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"
11786
12418
  response: JSONObject = {
11787
12419
  "profile": raw_apply.get("profile"),
11788
12420
  "ws_id": raw_apply.get("ws_id"),
@@ -11795,7 +12427,6 @@ class RecordTools(ToolBase):
11795
12427
  "warnings": warnings,
11796
12428
  "output_profile": output_profile,
11797
12429
  "update_route": update_route,
11798
- "tried_routes": tried_routes,
11799
12430
  "data": {
11800
12431
  "action": {"operation": operation, "executed": True},
11801
12432
  "resource": resource,
@@ -11807,9 +12438,11 @@ class RecordTools(ToolBase):
11807
12438
  "resolved_fields": resolved_fields,
11808
12439
  "human_review": self._record_write_human_review_payload(operation, enabled=human_review),
11809
12440
  "update_route": update_route,
11810
- "tried_routes": tried_routes,
11811
12441
  },
11812
12442
  }
12443
+ if expose_tried_routes:
12444
+ response["tried_routes"] = tried_routes
12445
+ response["data"]["tried_routes"] = tried_routes
11813
12446
  if record_id is not None:
11814
12447
  response["record_id"] = record_id
11815
12448
  if apply_id is not None:
@@ -11989,7 +12622,7 @@ class RecordTools(ToolBase):
11989
12622
  )
11990
12623
  return errors
11991
12624
 
11992
- def _record_delete_many(self, *, profile: str, app_key: str, record_ids: list[int]) -> JSONObject:
12625
+ def _record_delete_many(self, *, profile: str, app_key: str, record_ids: list[int], list_type: int = DEFAULT_RECORD_LIST_TYPE) -> JSONObject:
11993
12626
  """执行内部辅助逻辑。"""
11994
12627
  if not app_key:
11995
12628
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
@@ -12002,14 +12635,14 @@ class RecordTools(ToolBase):
12002
12635
  "DELETE",
12003
12636
  context,
12004
12637
  f"/app/{app_key}/apply",
12005
- json_body={"type": DEFAULT_RECORD_LIST_TYPE, "applyIds": normalized_ids},
12638
+ json_body={"type": list_type, "applyIds": normalized_ids},
12006
12639
  )
12007
12640
  return {
12008
12641
  "profile": profile,
12009
12642
  "ws_id": session_profile.selected_ws_id,
12010
12643
  "request_route": self._request_route_payload(context),
12011
12644
  "result": result,
12012
- "resource": {"type": "record", "apply_ids": normalized_ids},
12645
+ "resource": {"type": "record", "apply_ids": normalized_ids, "list_type": list_type},
12013
12646
  "ok": True,
12014
12647
  }
12015
12648
 
@@ -12583,6 +13216,30 @@ class RecordTools(ToolBase):
12583
13216
  },
12584
13217
  )
12585
13218
 
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
+
12586
13243
  def _candidate_keyword_from_value(
12587
13244
  self,
12588
13245
  value: JSONValue,
@@ -12847,14 +13504,7 @@ class RecordTools(ToolBase):
12847
13504
 
12848
13505
  def _lookup_department_detail(self, context, keyword: str, *, purpose: str) -> JSONObject: # type: ignore[no-untyped-def]
12849
13506
  """执行内部辅助逻辑。"""
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 []
13507
+ items = self._search_workspace_departments(context, keyword=keyword)
12858
13508
  normalized_keyword = keyword.strip()
12859
13509
  exact = [
12860
13510
  item for item in items
@@ -13379,6 +14029,7 @@ class RecordTools(ToolBase):
13379
14029
  normalized_answers: list[JSONObject],
13380
14030
  index: FieldIndex,
13381
14031
  verify_list_type: int = DEFAULT_RECORD_LIST_TYPE,
14032
+ verify_role: int | None = None,
13382
14033
  verify_view_key: str | None = None,
13383
14034
  ) -> JSONObject:
13384
14035
  """执行内部辅助逻辑。"""
@@ -13398,14 +14049,20 @@ class RecordTools(ToolBase):
13398
14049
  f"/view/{verify_view_key}/apply/{apply_id}",
13399
14050
  )
13400
14051
  else:
14052
+ role = verify_role if verify_role is not None else 1
13401
14053
  record = self.backend.request(
13402
14054
  "GET",
13403
14055
  context,
13404
14056
  f"/app/{app_key}/apply/{apply_id}",
13405
- params={"role": 1, "listType": verify_list_type},
14057
+ params={"role": role, "listType": verify_list_type},
13406
14058
  )
13407
14059
  except QingflowApiError as exc:
13408
14060
  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))
13409
14066
  return {
13410
14067
  "verified": False,
13411
14068
  "verification_mode": "custom_view_record_detail",
@@ -13414,14 +14071,9 @@ class RecordTools(ToolBase):
13414
14071
  "missing_fields": [],
13415
14072
  "empty_fields": [],
13416
14073
  "count_mismatches": [],
13417
- "warnings": [{
13418
- "code": "WRITE_VERIFY_CUSTOM_VIEW_READBACK_FAILED",
13419
- "message": "Write was sent through a custom view route, but the same view could not be re-read for field-level verification.",
13420
- "backend_code": exc.backend_code,
13421
- "http_status": exc.http_status,
13422
- }],
14074
+ "warnings": [warning],
13423
14075
  }
13424
- if exc.backend_code != 40002:
14076
+ if not _is_record_permission_denied_error(exc):
13425
14077
  raise
13426
14078
  return self._verify_record_write_result_via_initiated_tasks(
13427
14079
  context,
@@ -13469,6 +14121,7 @@ class RecordTools(ToolBase):
13469
14121
  or len(count_mismatches) > mismatch_before
13470
14122
  ):
13471
14123
  continue
14124
+ continue
13472
14125
  expected_value = _canonicalize_answer_value_for_compare(answer, field)
13473
14126
  actual_value = _canonicalize_answer_value_for_compare(actual, field)
13474
14127
  if not _canonical_value_is_empty(expected_value) and _canonical_value_is_empty(actual_value):
@@ -13715,6 +14368,8 @@ def _normalize_data_list_base_info_schema(payload: JSONValue) -> JSONObject:
13715
14368
  if not isinstance(payload, dict):
13716
14369
  return {}
13717
14370
  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")
13718
14373
  if not isinstance(que_base_infos, list):
13719
14374
  return {}
13720
14375
  return {
@@ -14101,6 +14756,44 @@ def _build_answer_backed_field_index(
14101
14756
  )
14102
14757
 
14103
14758
 
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
+
14104
14797
  def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
14105
14798
  by_id = dict(primary.by_id)
14106
14799
  by_title = {key: list(value) for key, value in primary.by_title.items()}
@@ -14111,12 +14804,42 @@ def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
14111
14804
 
14112
14805
  for field_id, field in extra.by_id.items():
14113
14806
  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)
14114
14812
  continue
14115
14813
  by_id[field_id] = field
14116
14814
  by_title.setdefault(_normalize_field_lookup_key(field.que_title), []).append(field)
14117
14815
  for alias in field.aliases:
14118
14816
  by_alias.setdefault(_normalize_field_lookup_key(alias), []).append(field)
14119
14817
 
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
+
14120
14843
  return FieldIndex(
14121
14844
  by_id=by_id,
14122
14845
  by_title=by_title,
@@ -15507,6 +16230,28 @@ def _record_detail_unavailable_context(section: str, message: str, exc: Qingflow
15507
16230
  "message": message,
15508
16231
  "category": exc.category,
15509
16232
  }
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"
15510
16255
  if exc.backend_code is not None:
15511
16256
  payload["backend_code"] = exc.backend_code
15512
16257
  if exc.http_status is not None:
@@ -15516,11 +16261,35 @@ def _record_detail_unavailable_context(section: str, message: str, exc: Qingflow
15516
16261
  payload["request_id"] = request_id
15517
16262
  details = exc.details if isinstance(exc.details, dict) else {}
15518
16263
  error_code = details.get("error_code")
15519
- if error_code:
16264
+ if error_code and not payload.get("error_code"):
15520
16265
  payload["error_code"] = error_code
15521
16266
  return payload
15522
16267
 
15523
16268
 
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
+
15524
16293
  _RECORD_MEDIA_IMG_SRC_RE = re.compile(r"""<img\b[^>]*\bsrc\s*=\s*["']?([^"'\s>]+)""", re.IGNORECASE)
15525
16294
  _RECORD_MEDIA_MD_IMAGE_RE = re.compile(r"""!\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)""")
15526
16295
  _RECORD_MEDIA_URL_RE = re.compile(r"""https?://[^\s<>"')\]]+""", re.IGNORECASE)
@@ -15675,7 +16444,14 @@ def _record_detail_media_assets_payload(
15675
16444
  except QingflowApiError as exc:
15676
16445
  blocked = exc.http_status in {401, 403}
15677
16446
  if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
15678
- refreshed_url = _normalize_optional_text(refresh_source_url(candidate))
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)
15679
16455
  if refreshed_url and refreshed_url != source_url:
15680
16456
  refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
15681
16457
  try:
@@ -15717,14 +16493,13 @@ def _record_detail_media_assets_payload(
15717
16493
  "readable_by_agent": False,
15718
16494
  }
15719
16495
  )
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
- )
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)
15728
16503
  continue
15729
16504
 
15730
16505
  if not isinstance(content, bytes):
@@ -15942,7 +16717,14 @@ def _record_detail_file_assets_payload(
15942
16717
  except QingflowApiError as exc:
15943
16718
  blocked = exc.http_status in {401, 403}
15944
16719
  if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
15945
- refreshed_url = _normalize_optional_text(refresh_source_url(candidate))
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)
15946
16728
  if refreshed_url and refreshed_url != source_url:
15947
16729
  refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
15948
16730
  try:
@@ -15989,14 +16771,13 @@ def _record_detail_file_assets_payload(
15989
16771
  "extraction": {"status": "failed", "text_path": None, "preview": None},
15990
16772
  }
15991
16773
  )
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
- )
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)
16000
16781
  continue
16001
16782
 
16002
16783
  if not isinstance(content, bytes):
@@ -18115,12 +18896,14 @@ def _extract_sort_selector(item: JSONObject) -> JSONValue:
18115
18896
  return None
18116
18897
 
18117
18898
 
18118
- def _normalize_public_column_selectors(columns: list[JSONObject | int]) -> list[int]:
18899
+ def _normalize_public_column_selectors(columns: list[JSONObject | int | str]) -> list[int]:
18119
18900
  normalized: list[int] = []
18120
18901
  for item in columns:
18121
18902
  field_id: int | None = None
18122
18903
  if isinstance(item, int):
18123
18904
  field_id = item
18905
+ elif isinstance(item, str):
18906
+ field_id = _coerce_count(item)
18124
18907
  elif isinstance(item, dict):
18125
18908
  _ensure_allowed_record_list_keys(
18126
18909
  item,
@@ -18132,19 +18915,21 @@ def _normalize_public_column_selectors(columns: list[JSONObject | int]) -> list[
18132
18915
  if field_id is None or field_id < 0:
18133
18916
  raise_tool_error(
18134
18917
  QingflowApiError.config_error(
18135
- "columns must be a list of field_id integers or {field_id} objects"
18918
+ "columns must be a list of field_id integers, integer strings, or {field_id} objects"
18136
18919
  )
18137
18920
  )
18138
18921
  normalized.append(field_id)
18139
18922
  return normalized
18140
18923
 
18141
18924
 
18142
- def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int]) -> list[int]:
18925
+ def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int | str]) -> list[int]:
18143
18926
  normalized: list[int] = []
18144
18927
  for item in query_fields:
18145
18928
  field_id: int | None = None
18146
18929
  if isinstance(item, int):
18147
18930
  field_id = item
18931
+ elif isinstance(item, str):
18932
+ field_id = _coerce_count(item)
18148
18933
  elif isinstance(item, dict):
18149
18934
  _ensure_allowed_record_list_keys(
18150
18935
  item,
@@ -18156,7 +18941,7 @@ def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int]
18156
18941
  if field_id is None or field_id < 0:
18157
18942
  raise_tool_error(
18158
18943
  QingflowApiError.config_error(
18159
- "query_fields must be a list of field_id integers or {field_id} objects"
18944
+ "query_fields must be a list of field_id integers, integer strings, or {field_id} objects"
18160
18945
  )
18161
18946
  )
18162
18947
  normalized.append(field_id)