@josephyan/qingflow-cli 1.1.4 → 1.1.6

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 +4192 -935
  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
@@ -9,12 +9,12 @@ from mcp.server.fastmcp import FastMCP
9
9
 
10
10
  from ..backend_client import BackendRequestContext
11
11
  from ..config import DEFAULT_PROFILE
12
- from ..errors import QingflowApiError, raise_tool_error
12
+ from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error, message_looks_like_invalid_token, raise_tool_error
13
13
  from ..id_utils import ids_equal, normalize_positive_id_int, normalize_positive_id_text, stringify_backend_id
14
14
  from ..json_types import JSONObject
15
15
  from .approval_tools import ApprovalTools, _approval_page_amount, _approval_page_items, _approval_page_total
16
16
  from .base import ToolBase, tool_cn_name
17
- from .qingbi_report_tools import _qingbi_base_url
17
+ from .qingbi_report_tools import _qingbi_base_url, _should_retry_asos_data
18
18
  from .record_tools import (
19
19
  FieldIndex,
20
20
  LAYOUT_ONLY_QUE_TYPES,
@@ -38,6 +38,9 @@ from .record_tools import (
38
38
  from .task_tools import TaskTools, _task_page_amount, _task_page_items, _task_page_total
39
39
 
40
40
 
41
+ TASK_LOCATOR_MAX_SCAN_PAGES_PER_BOX = 3
42
+
43
+
41
44
  class TaskContextTools(ToolBase):
42
45
  """任务上下文工具(中文名:任务上下文与审批执行)。
43
46
 
@@ -85,33 +88,37 @@ class TaskContextTools(ToolBase):
85
88
  page_size=page_size,
86
89
  )
87
90
 
88
- @mcp.tool()
91
+ @mcp.tool(
92
+ description=(
93
+ "Read one workflow task. Prefer task_id from task_list.data.items[].task_id; "
94
+ "task_id is not a row number, list index, record id, or workflow node id."
95
+ )
96
+ )
89
97
  def task_get(
90
98
  profile: str = DEFAULT_PROFILE,
91
99
  task_id: str = "",
92
- app_key: str = "",
93
- record_id: str = "",
94
- workflow_node_id: int = 0,
95
100
  include_candidates: bool = True,
96
101
  include_associated_reports: bool = True,
97
102
  ) -> dict[str, Any]:
98
103
  return self.task_get(
99
104
  profile=profile,
100
105
  task_id=task_id,
101
- app_key=app_key,
102
- record_id=record_id,
103
- workflow_node_id=workflow_node_id,
106
+ app_key="",
107
+ record_id="",
108
+ workflow_node_id=0,
104
109
  include_candidates=include_candidates,
105
110
  include_associated_reports=include_associated_reports,
106
111
  )
107
112
 
108
- @mcp.tool(description=self._high_risk_tool_description(operation="execute", target="workflow task action"))
113
+ @mcp.tool(
114
+ description=(
115
+ self._high_risk_tool_description(operation="execute", target="workflow task action")
116
+ + " Pass task_id from task_list.data.items[].task_id. Do not pass a row number, list index, record id, or workflow node id as task_id."
117
+ )
118
+ )
109
119
  def task_action_execute(
110
120
  profile: str = DEFAULT_PROFILE,
111
121
  task_id: str = "",
112
- app_key: str = "",
113
- record_id: str = "",
114
- workflow_node_id: int = 0,
115
122
  action: str = "",
116
123
  payload: dict[str, Any] | None = None,
117
124
  fields: dict[str, Any] | None = None,
@@ -119,21 +126,20 @@ class TaskContextTools(ToolBase):
119
126
  return self.task_action_execute(
120
127
  profile=profile,
121
128
  task_id=task_id,
122
- app_key=app_key,
123
- record_id=record_id,
124
- workflow_node_id=workflow_node_id,
125
129
  action=action,
126
130
  payload=payload or {},
127
131
  fields=fields or {},
128
132
  )
129
133
 
130
- @mcp.tool()
134
+ @mcp.tool(
135
+ description=(
136
+ "Read a task-associated report. Pass task_id from task_list.data.items[].task_id; "
137
+ "task_id is not a row number, list index, record id, or workflow node id."
138
+ )
139
+ )
131
140
  def task_associated_report_detail_get(
132
141
  profile: str = DEFAULT_PROFILE,
133
142
  task_id: str = "",
134
- app_key: str = "",
135
- record_id: str = "",
136
- workflow_node_id: int = 0,
137
143
  report_id: int = 0,
138
144
  page: int = 1,
139
145
  page_size: int = 20,
@@ -141,28 +147,30 @@ class TaskContextTools(ToolBase):
141
147
  return self.task_associated_report_detail_get(
142
148
  profile=profile,
143
149
  task_id=task_id,
144
- app_key=app_key,
145
- record_id=record_id,
146
- workflow_node_id=workflow_node_id,
150
+ app_key="",
151
+ record_id="",
152
+ workflow_node_id=0,
147
153
  report_id=report_id,
148
154
  page=page,
149
155
  page_size=page_size,
150
156
  )
151
157
 
152
- @mcp.tool()
158
+ @mcp.tool(
159
+ description=(
160
+ "Read workflow log for one task context. Pass task_id from task_list.data.items[].task_id; "
161
+ "task_id is not a row number, list index, record id, or workflow node id."
162
+ )
163
+ )
153
164
  def task_workflow_log_get(
154
165
  profile: str = DEFAULT_PROFILE,
155
166
  task_id: str = "",
156
- app_key: str = "",
157
- record_id: str = "",
158
- workflow_node_id: int = 0,
159
167
  ) -> dict[str, Any]:
160
168
  return self.task_workflow_log_get(
161
169
  profile=profile,
162
170
  task_id=task_id,
163
- app_key=app_key,
164
- record_id=record_id,
165
- workflow_node_id=workflow_node_id,
171
+ app_key="",
172
+ record_id="",
173
+ workflow_node_id=0,
166
174
  )
167
175
 
168
176
  @tool_cn_name("任务上下文列表")
@@ -271,6 +279,7 @@ class TaskContextTools(ToolBase):
271
279
  resolved_app_key = str(locator["app_key"])
272
280
  resolved_record_id = int(locator["record_id"])
273
281
  resolved_workflow_node_id = int(locator["workflow_node_id"])
282
+ resolved_task_box = str(locator.get("task_box") or "todo")
274
283
  self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
275
284
  data = self._build_task_context(
276
285
  profile=profile,
@@ -278,10 +287,12 @@ class TaskContextTools(ToolBase):
278
287
  app_key=resolved_app_key,
279
288
  record_id=resolved_record_id,
280
289
  workflow_node_id=resolved_workflow_node_id,
290
+ task_box=resolved_task_box,
281
291
  include_candidates=include_candidates,
282
292
  include_associated_reports=include_associated_reports,
283
293
  current_uid=session_profile.uid,
284
294
  )
295
+ context_warnings = data.get("warnings") if isinstance(data.get("warnings"), list) else []
285
296
  data = self._compact_task_get_context(data)
286
297
  task_payload = data.get("task")
287
298
  if isinstance(task_payload, dict) and task_id_text is not None:
@@ -291,7 +302,7 @@ class TaskContextTools(ToolBase):
291
302
  "ws_id": session_profile.selected_ws_id,
292
303
  "ok": True,
293
304
  "request_route": self._request_route_payload(context),
294
- "warnings": [],
305
+ "warnings": context_warnings,
295
306
  "output_profile": "normal",
296
307
  "data": data,
297
308
  }
@@ -303,9 +314,7 @@ class TaskContextTools(ToolBase):
303
314
  self,
304
315
  *,
305
316
  profile: str,
306
- app_key: str,
307
- record_id: Any,
308
- workflow_node_id: int,
317
+ task_id: Any,
309
318
  fields: dict[str, Any] | None = None,
310
319
  ) -> dict[str, Any]:
311
320
  """执行任务相关逻辑。"""
@@ -314,9 +323,7 @@ class TaskContextTools(ToolBase):
314
323
  raise_tool_error(QingflowApiError.config_error("fields is required and must be non-empty for task_save_only"))
315
324
  return self.task_action_execute(
316
325
  profile=profile,
317
- app_key=app_key,
318
- record_id=record_id,
319
- workflow_node_id=workflow_node_id,
326
+ task_id=task_id,
320
327
  action="save_only",
321
328
  payload={},
322
329
  fields=field_updates,
@@ -324,6 +331,34 @@ class TaskContextTools(ToolBase):
324
331
 
325
332
  @tool_cn_name("执行任务动作")
326
333
  def task_action_execute(
334
+ self,
335
+ *,
336
+ profile: str,
337
+ task_id: Any,
338
+ action: str,
339
+ payload: dict[str, Any],
340
+ fields: dict[str, Any] | None = None,
341
+ ) -> dict[str, Any]:
342
+ """执行任务相关逻辑。"""
343
+ if task_id in (None, ""):
344
+ raise_tool_error(
345
+ QingflowApiError.config_error(
346
+ "task_id is required for task_action_execute; get it from task_list.data.items[].task_id"
347
+ )
348
+ )
349
+
350
+ return self._task_action_execute_with_locator(
351
+ profile=profile,
352
+ task_id=task_id,
353
+ app_key="",
354
+ record_id="",
355
+ workflow_node_id=0,
356
+ action=action,
357
+ payload=payload,
358
+ fields=fields,
359
+ )
360
+
361
+ def _task_action_execute_with_locator(
327
362
  self,
328
363
  *,
329
364
  profile: str,
@@ -335,10 +370,8 @@ class TaskContextTools(ToolBase):
335
370
  payload: dict[str, Any],
336
371
  fields: dict[str, Any] | None = None,
337
372
  ) -> dict[str, Any]:
338
- """执行任务相关逻辑。"""
339
373
  if task_id in (None, ""):
340
374
  normalize_positive_id_int(record_id, field_name="record_id")
341
-
342
375
  normalized_action = (action or "").strip().lower()
343
376
  if normalized_action not in {"approve", "reject", "rollback", "transfer", "urge", "save_only"}:
344
377
  raise_tool_error(
@@ -368,8 +401,63 @@ class TaskContextTools(ToolBase):
368
401
  resolved_record_id = int(locator["record_id"])
369
402
  resolved_record_id_text = str(locator["record_id_text"] or "")
370
403
  resolved_workflow_node_id = int(locator["workflow_node_id"])
404
+ resolved_task_box = str(locator.get("task_box") or "todo")
371
405
  record_id_text = resolved_record_id_text
372
406
  self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
407
+ if normalized_action == "urge":
408
+ raw = self._execute_task_action(
409
+ profile=profile,
410
+ app_key=resolved_app_key,
411
+ record_id=resolved_record_id,
412
+ workflow_node_id=resolved_workflow_node_id,
413
+ normalized_action=normalized_action,
414
+ payload=body,
415
+ prepared_fields=None,
416
+ )
417
+ verification, verification_warnings = self._verify_task_action_runtime(
418
+ profile=profile,
419
+ context=context,
420
+ app_key=resolved_app_key,
421
+ record_id=resolved_record_id,
422
+ workflow_node_id=resolved_workflow_node_id,
423
+ action=normalized_action,
424
+ before_apply_status=None,
425
+ runtime_baseline=None,
426
+ )
427
+ result = {
428
+ "profile": raw.get("profile", profile),
429
+ "ws_id": raw.get("ws_id", session_profile.selected_ws_id),
430
+ "ok": bool(raw.get("ok", True)),
431
+ "status": "success",
432
+ "error_code": None,
433
+ "action_executed": True,
434
+ "safe_to_retry": False,
435
+ "request_route": raw.get("request_route") or self._request_route_payload(context),
436
+ "warnings": verification_warnings,
437
+ "verification": verification,
438
+ "output_profile": "normal",
439
+ "data": {
440
+ "action": normalized_action,
441
+ "resource": {
442
+ "app_key": resolved_app_key,
443
+ "record_id": record_id_text,
444
+ "workflow_node_id": resolved_workflow_node_id,
445
+ },
446
+ "selection": {"action": normalized_action},
447
+ "result": raw.get("result"),
448
+ "human_review": True,
449
+ "field_update_applied": False,
450
+ },
451
+ }
452
+ if task_id_text is not None:
453
+ result["data"]["resource"]["task_id"] = task_id_text
454
+ return result
455
+ if resolved_task_box != "todo":
456
+ raise_tool_error(
457
+ QingflowApiError.config_error(
458
+ f"task_id={task_id_text or ''} resolved to task_box='{resolved_task_box}', but task_action_execute can only execute current todo tasks"
459
+ )
460
+ )
373
461
  try:
374
462
  task_context = self._build_task_context(
375
463
  profile=profile,
@@ -377,12 +465,13 @@ class TaskContextTools(ToolBase):
377
465
  app_key=resolved_app_key,
378
466
  record_id=resolved_record_id,
379
467
  workflow_node_id=resolved_workflow_node_id,
468
+ task_box=resolved_task_box,
380
469
  include_candidates=False,
381
470
  include_associated_reports=False,
382
471
  current_uid=session_profile.uid,
383
472
  )
384
473
  except QingflowApiError as error:
385
- if error.backend_code == 46001:
474
+ if backend_code_int(error) == 46001:
386
475
  return self._task_action_visibility_unverified_response(
387
476
  profile=profile,
388
477
  session_profile=session_profile,
@@ -422,7 +511,7 @@ class TaskContextTools(ToolBase):
422
511
  raise_tool_error(QingflowApiError.config_error(message))
423
512
  raise_tool_error(
424
513
  QingflowApiError.config_error(
425
- f"task action '{normalized_action}' is not currently available for app_key='{resolved_app_key}' record_id={record_id_text} workflow_node_id={resolved_workflow_node_id}"
514
+ f"task action '{normalized_action}' is not currently available for task_id='{task_id_text}'"
426
515
  )
427
516
  )
428
517
  feedback_required_for = capabilities.get("action_constraints", {}).get("feedback_required_for") or []
@@ -470,7 +559,7 @@ class TaskContextTools(ToolBase):
470
559
  prepared_fields=prepared_fields,
471
560
  )
472
561
  except QingflowApiError as error:
473
- if error.backend_code == 46001:
562
+ if backend_code_int(error) == 46001:
474
563
  return self._task_action_visibility_unverified_response(
475
564
  profile=profile,
476
565
  session_profile=session_profile,
@@ -518,6 +607,8 @@ class TaskContextTools(ToolBase):
518
607
  "ok": bool(raw.get("ok", True)) and status != "failed",
519
608
  "status": status,
520
609
  "error_code": error_code,
610
+ "action_executed": True,
611
+ "safe_to_retry": False,
521
612
  "request_route": raw.get("request_route") or self._request_route_payload(context),
522
613
  "warnings": warnings,
523
614
  "verification": verification,
@@ -680,6 +771,8 @@ class TaskContextTools(ToolBase):
680
771
  "http_status": error.http_status,
681
772
  "backend_code": error.backend_code,
682
773
  "category": error.category,
774
+ "request_id": error.request_id,
775
+ "message": error.message,
683
776
  }
684
777
 
685
778
  log_items: list[dict[str, Any]] = []
@@ -707,6 +800,8 @@ class TaskContextTools(ToolBase):
707
800
  "http_status": error.http_status,
708
801
  "backend_code": error.backend_code,
709
802
  "category": error.category,
803
+ "request_id": error.request_id,
804
+ "message": error.message,
710
805
  }
711
806
 
712
807
  todo_items = self._safe_task_list_items(profile=profile, task_box="todo", app_key=app_key)
@@ -756,7 +851,7 @@ class TaskContextTools(ToolBase):
756
851
  runtime_consumed_after_action = bool(
757
852
  runtime_verified
758
853
  and isinstance(record_state_error, dict)
759
- and record_state_error.get("backend_code") == 46001
854
+ and backend_code_value_int(record_state_error.get("backend_code")) == 46001
760
855
  )
761
856
  if runtime_consumed_after_action:
762
857
  verification["record_state_scope"] = "current_node_runtime"
@@ -771,6 +866,28 @@ class TaskContextTools(ToolBase):
771
866
  ),
772
867
  }
773
868
  )
869
+ permission_blocked_sources: list[str] = []
870
+ if (
871
+ isinstance(verification.get("record_state_error"), dict)
872
+ and _is_permission_context_error_payload(verification["record_state_error"])
873
+ ):
874
+ permission_blocked_sources.append("record_state")
875
+ if (
876
+ isinstance(verification.get("workflow_log_error"), dict)
877
+ and _is_permission_context_error_payload(verification["workflow_log_error"])
878
+ ):
879
+ permission_blocked_sources.append("workflow_log")
880
+ if permission_blocked_sources:
881
+ warnings.append(
882
+ {
883
+ "code": "TASK_ACTION_VERIFICATION_PERMISSION_UNAVAILABLE",
884
+ "message": (
885
+ "task action executed, but some post-action verification reads are unavailable "
886
+ "in this permission context; do not treat the verification read failure as action denial."
887
+ ),
888
+ "sources": permission_blocked_sources,
889
+ }
890
+ )
774
891
  verification["runtime_continuation_verified"] = runtime_verified
775
892
  if not runtime_verified:
776
893
  warnings.append(
@@ -931,7 +1048,9 @@ class TaskContextTools(ToolBase):
931
1048
  baseline["workflow_log_visible"] = True
932
1049
  baseline["workflow_log_count"] = len(log_items)
933
1050
  baseline["workflow_log_digest"] = self._workflow_log_digest(log_items)
934
- except QingflowApiError:
1051
+ except QingflowApiError as exc:
1052
+ if is_auth_like_error(exc):
1053
+ raise
935
1054
  pass
936
1055
  todo_items = self._safe_task_list_items(profile=profile, task_box="todo", app_key=app_key)
937
1056
  baseline["downstream_todo_nodes"] = sorted(
@@ -1058,7 +1177,9 @@ class TaskContextTools(ToolBase):
1058
1177
  page=1,
1059
1178
  page_size=50,
1060
1179
  )
1061
- except QingflowApiError:
1180
+ except QingflowApiError as exc:
1181
+ if not _is_task_optional_read_error(exc):
1182
+ raise
1062
1183
  return []
1063
1184
  items = response.get("items") if isinstance(response, dict) else None
1064
1185
  if not isinstance(items, list):
@@ -1152,21 +1273,36 @@ class TaskContextTools(ToolBase):
1152
1273
  task_id_text = normalize_positive_id_text(task_id, field_name="task_id")
1153
1274
  searched_task_boxes = ("todo", "initiated", "cc", "done")
1154
1275
  incomplete_task_boxes: list[str] = []
1276
+ inaccessible_task_boxes: list[dict[str, Any]] = []
1277
+ truncated_task_boxes: list[dict[str, Any]] = []
1155
1278
  page_size = 100
1156
1279
  for task_box in searched_task_boxes:
1157
1280
  page = 1
1158
1281
  page_amount: int | None = None
1159
1282
  while True:
1160
- response = self._list_normalized_task_items(
1161
- profile=profile,
1162
- task_box=task_box,
1163
- flow_status="all",
1164
- app_key=None,
1165
- workflow_node_id=None,
1166
- query=None,
1167
- page=page,
1168
- page_size=page_size,
1169
- )
1283
+ try:
1284
+ response = self._list_normalized_task_items(
1285
+ profile=profile,
1286
+ task_box=task_box,
1287
+ flow_status="all",
1288
+ app_key=None,
1289
+ workflow_node_id=None,
1290
+ query=None,
1291
+ page=page,
1292
+ page_size=page_size,
1293
+ )
1294
+ except QingflowApiError as exc:
1295
+ if not _is_optional_task_box_locator_error(exc):
1296
+ raise
1297
+ inaccessible_task_boxes.append(
1298
+ {
1299
+ "task_box": task_box,
1300
+ "backend_code": exc.backend_code,
1301
+ "http_status": exc.http_status,
1302
+ "request_id": exc.request_id,
1303
+ }
1304
+ )
1305
+ break
1170
1306
  items = response.get("items") if isinstance(response.get("items"), list) else []
1171
1307
  for item in items:
1172
1308
  if not isinstance(item, dict) or not ids_equal(item.get("task_id"), task_id_text):
@@ -1192,17 +1328,52 @@ class TaskContextTools(ToolBase):
1192
1328
  break
1193
1329
  if not items or len(items) < page_size:
1194
1330
  break
1331
+ if page >= TASK_LOCATOR_MAX_SCAN_PAGES_PER_BOX:
1332
+ truncated_task_boxes.append(
1333
+ {
1334
+ "task_box": task_box,
1335
+ "max_pages": TASK_LOCATOR_MAX_SCAN_PAGES_PER_BOX,
1336
+ "page_size": page_size,
1337
+ "reported_page_amount": page_amount,
1338
+ }
1339
+ )
1340
+ break
1195
1341
  page += 1
1196
1342
  if incomplete_task_boxes:
1197
1343
  searched = ", ".join(incomplete_task_boxes)
1198
1344
  raise_tool_error(
1199
1345
  QingflowApiError.config_error(
1200
- f"task_id={task_id_text} resolved to an incomplete task locator in task_box={searched}; please refresh the task list and retry"
1346
+ f"task_id={task_id_text} resolved to an incomplete task locator in task_box={searched}; rerun task_list and pass the exact data.items[].task_id. Do not substitute a row number or rebuild the locator from app_key/record_id/workflow_node_id."
1347
+ )
1348
+ )
1349
+ if inaccessible_task_boxes:
1350
+ searched = ", ".join(str(item.get("task_box")) for item in inaccessible_task_boxes)
1351
+ raise_tool_error(
1352
+ QingflowApiError.config_error(
1353
+ f"task_id={task_id_text} was not found in visible task boxes; some task boxes were not searchable in the current permission context: {searched}. Rerun task_list and use data.items[].task_id; do not substitute a row number or rebuild the locator.",
1354
+ details={
1355
+ "task_id": task_id_text,
1356
+ "searched_task_boxes": list(searched_task_boxes),
1357
+ "inaccessible_task_boxes": inaccessible_task_boxes,
1358
+ },
1359
+ )
1360
+ )
1361
+ if truncated_task_boxes:
1362
+ raise_tool_error(
1363
+ QingflowApiError.config_error(
1364
+ f"task_id={task_id_text} was not found in the first {TASK_LOCATOR_MAX_SCAN_PAGES_PER_BOX} pages of each visible task box. Rerun task_list with a query or the relevant page and pass the exact data.items[].task_id; do not guess task ids.",
1365
+ details={
1366
+ "task_id": task_id_text,
1367
+ "searched_task_boxes": list(searched_task_boxes),
1368
+ "truncated_task_boxes": truncated_task_boxes,
1369
+ "max_pages_per_task_box": TASK_LOCATOR_MAX_SCAN_PAGES_PER_BOX,
1370
+ "page_size": page_size,
1371
+ },
1201
1372
  )
1202
1373
  )
1203
1374
  raise_tool_error(
1204
1375
  QingflowApiError.config_error(
1205
- f"task_id={task_id_text} was not found in the current visible task boxes (todo, initiated, cc, done)"
1376
+ f"task_id={task_id_text} was not found in the current visible task boxes (todo, initiated, cc, done). Rerun task_list and pass data.items[].task_id; do not use the displayed row number or guess app_key/record_id/workflow_node_id."
1206
1377
  )
1207
1378
  )
1208
1379
 
@@ -1224,6 +1395,7 @@ class TaskContextTools(ToolBase):
1224
1395
  resolved_app_key = str(locator["app_key"])
1225
1396
  resolved_record_id = normalize_positive_id_int(locator["record_id"], field_name="record_id")
1226
1397
  resolved_workflow_node_id = int(locator["workflow_node_id"])
1398
+ resolved_task_box = str(locator.get("task_box") or "todo")
1227
1399
  explicit_app_key = (app_key or "").strip()
1228
1400
  if explicit_app_key and explicit_app_key != resolved_app_key:
1229
1401
  raise_tool_error(
@@ -1248,8 +1420,10 @@ class TaskContextTools(ToolBase):
1248
1420
  else:
1249
1421
  resolved_record_id = normalize_positive_id_int(record_id, field_name="record_id")
1250
1422
  resolved_workflow_node_id = int(workflow_node_id)
1423
+ resolved_task_box = "todo"
1251
1424
  return {
1252
1425
  "task_id": task_id_text,
1426
+ "task_box": resolved_task_box,
1253
1427
  "app_key": resolved_app_key,
1254
1428
  "record_id": resolved_record_id,
1255
1429
  "record_id_text": stringify_backend_id(resolved_record_id),
@@ -1307,6 +1481,7 @@ class TaskContextTools(ToolBase):
1307
1481
  resolved_record_id = int(locator["record_id"])
1308
1482
  record_id_text = str(locator["record_id_text"] or "")
1309
1483
  resolved_workflow_node_id = int(locator["workflow_node_id"])
1484
+ resolved_task_box = str(locator.get("task_box") or "todo")
1310
1485
  self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
1311
1486
  task_context = self._build_task_context(
1312
1487
  profile=profile,
@@ -1314,6 +1489,7 @@ class TaskContextTools(ToolBase):
1314
1489
  app_key=resolved_app_key,
1315
1490
  record_id=resolved_record_id,
1316
1491
  workflow_node_id=resolved_workflow_node_id,
1492
+ task_box=resolved_task_box,
1317
1493
  include_candidates=False,
1318
1494
  include_associated_reports=True,
1319
1495
  current_uid=session_profile.uid,
@@ -1387,7 +1563,7 @@ class TaskContextTools(ToolBase):
1387
1563
 
1388
1564
  chart_key = str(report_item.get("chart_key") or "")
1389
1565
  source_type = report_item.get("source_type")
1390
- if source_type == "qingbi":
1566
+ if source_type in {"qingbi", "bi_qingflow", "bi_dataset"}:
1391
1567
  qingbi_context = BackendRequestContext(
1392
1568
  base_url=_qingbi_base_url(context.base_url),
1393
1569
  token=context.token,
@@ -1396,20 +1572,44 @@ class TaskContextTools(ToolBase):
1396
1572
  qf_version=context.qf_version,
1397
1573
  qf_version_source=context.qf_version_source,
1398
1574
  )
1399
- chart_result = self.backend.request(
1400
- "POST",
1401
- qingbi_context,
1402
- f"/qingbi/charts/data/{chart_key}",
1403
- params={
1404
- "qfUUID": uuid4().hex,
1405
- "pageNum": page,
1406
- "pageSize": page_size,
1407
- },
1408
- json_body={
1409
- "asosChartId": report_id,
1410
- "keyQueValues": association_query.get("keyQueValues") or [],
1411
- },
1412
- )
1575
+ chart_payload = {
1576
+ "asosChartId": report_id,
1577
+ "keyQueValues": association_query.get("keyQueValues") or [],
1578
+ }
1579
+ chart_params = {
1580
+ "qfUUID": uuid4().hex,
1581
+ "pageNum": page,
1582
+ "pageSize": page_size,
1583
+ }
1584
+ try:
1585
+ chart_result = self.backend.request(
1586
+ "POST",
1587
+ qingbi_context,
1588
+ f"/qingbi/charts/data/qflow/{chart_key}/detail",
1589
+ params=chart_params,
1590
+ json_body=chart_payload,
1591
+ )
1592
+ except QingflowApiError as error:
1593
+ if not _should_retry_asos_data(error):
1594
+ raise
1595
+ try:
1596
+ chart_result = self.backend.request(
1597
+ "POST",
1598
+ qingbi_context,
1599
+ f"/qingbi/charts/data/qflow/{chart_key}/asos",
1600
+ params=chart_params,
1601
+ json_body=chart_payload,
1602
+ )
1603
+ except QingflowApiError as fallback_error:
1604
+ if not _should_retry_asos_data(fallback_error):
1605
+ raise
1606
+ chart_result = self.backend.request(
1607
+ "POST",
1608
+ qingbi_context,
1609
+ f"/qingbi/charts/data/{chart_key}",
1610
+ params=chart_params,
1611
+ json_body=chart_payload,
1612
+ )
1413
1613
  request_route = {
1414
1614
  "base_url": qingbi_context.base_url,
1415
1615
  "qf_version": qingbi_context.qf_version,
@@ -1509,6 +1709,7 @@ class TaskContextTools(ToolBase):
1509
1709
  resolved_record_id = int(locator["record_id"])
1510
1710
  record_id_text = str(locator["record_id_text"] or "")
1511
1711
  resolved_workflow_node_id = int(locator["workflow_node_id"])
1712
+ resolved_task_box = str(locator.get("task_box") or "todo")
1512
1713
  self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
1513
1714
  task_context = self._build_task_context(
1514
1715
  profile=profile,
@@ -1516,12 +1717,16 @@ class TaskContextTools(ToolBase):
1516
1717
  app_key=resolved_app_key,
1517
1718
  record_id=resolved_record_id,
1518
1719
  workflow_node_id=resolved_workflow_node_id,
1720
+ task_box=resolved_task_box,
1519
1721
  include_candidates=False,
1520
1722
  include_associated_reports=False,
1521
1723
  current_uid=session_profile.uid,
1522
1724
  )
1523
1725
  visibility = task_context.get("visibility") or {}
1524
- if not visibility.get("audit_record_visible"):
1726
+ node = task_context.get("node") if isinstance(task_context.get("node"), dict) else {}
1727
+ raw_node = node.get("raw") if isinstance(node.get("raw"), dict) else {}
1728
+ audit_visibility_explicit = "auditRecordVisible" in raw_node
1729
+ if audit_visibility_explicit and not visibility.get("audit_record_visible"):
1525
1730
  raise_tool_error(
1526
1731
  QingflowApiError.config_error(
1527
1732
  f"workflow logs are not visible for app_key='{resolved_app_key}' record_id={record_id_text} workflow_node_id={resolved_workflow_node_id}"
@@ -1535,7 +1740,7 @@ class TaskContextTools(ToolBase):
1535
1740
  "key": resolved_app_key,
1536
1741
  "rowRecordId": resolved_record_id,
1537
1742
  "nodeId": resolved_workflow_node_id,
1538
- "role": 3,
1743
+ "role": _task_box_record_role(resolved_task_box),
1539
1744
  "pageNum": 1,
1540
1745
  "pageSize": 200,
1541
1746
  },
@@ -1546,7 +1751,7 @@ class TaskContextTools(ToolBase):
1546
1751
  "ws_id": session_profile.selected_ws_id,
1547
1752
  "ok": True,
1548
1753
  "request_route": self._request_route_payload(context),
1549
- "warnings": [],
1754
+ "warnings": task_context.get("warnings") or [],
1550
1755
  "output_profile": "normal",
1551
1756
  "data": {
1552
1757
  "selection": {
@@ -1555,7 +1760,9 @@ class TaskContextTools(ToolBase):
1555
1760
  "workflow_node_id": resolved_workflow_node_id,
1556
1761
  },
1557
1762
  "visibility": {
1558
- "audit_record_visible": visibility.get("audit_record_visible"),
1763
+ "audit_record_visible": (
1764
+ visibility.get("audit_record_visible") if audit_visibility_explicit else None
1765
+ ),
1559
1766
  "qrobot_record_visible": visibility.get("qrobot_record_visible"),
1560
1767
  },
1561
1768
  "items": items,
@@ -1580,43 +1787,94 @@ class TaskContextTools(ToolBase):
1580
1787
  include_candidates: bool,
1581
1788
  include_associated_reports: bool,
1582
1789
  current_uid: int | None = None,
1790
+ task_box: str = "todo",
1583
1791
  ) -> dict[str, Any]:
1584
1792
  """执行内部辅助逻辑。"""
1585
- audit_infos = self.backend.request(
1586
- "GET",
1587
- context,
1588
- f"/app/{app_key}/apply/{record_id}/auditInfo",
1589
- params={"type": 1},
1590
- )
1591
- node_info = self._select_task_node(audit_infos, workflow_node_id, app_key=app_key, record_id=record_id)
1592
- detail = self.backend.request(
1593
- "GET",
1594
- context,
1595
- f"/app/{app_key}/apply/{record_id}",
1596
- params={"role": 3, "listType": 1, "auditNodeId": workflow_node_id},
1597
- )
1598
- app_name = self._task_app_name(context=context, app_key=app_key, detail=detail, node_info=node_info)
1599
- associated_report_visible = self._resolve_associated_report_visible(node_info, detail)
1600
- associated_reports = {"visible": associated_report_visible, "loaded": False, "count": 0, "items": []}
1601
- if include_associated_reports and associated_report_visible:
1602
- asos_chart_list = self.backend.request(
1793
+ context_warnings: list[JSONObject] = []
1794
+ detail: dict[str, Any] | None = None
1795
+ list_type = _task_box_record_list_type(task_box)
1796
+ role = _task_box_record_role(task_box)
1797
+ try:
1798
+ audit_infos = self.backend.request(
1603
1799
  "GET",
1604
1800
  context,
1605
- f"/app/{app_key}/asosChart",
1606
- params={"role": 3, "auditNodeId": workflow_node_id, "beingDraft": False},
1801
+ f"/app/{app_key}/apply/{record_id}/auditInfo",
1802
+ params={"type": list_type},
1607
1803
  )
1608
- associated_items = [
1609
- self._normalize_associated_report(item)
1610
- for item in (asos_chart_list.get("asosCharts") or [])
1611
- if isinstance(item, dict)
1612
- ]
1613
- associated_reports = {
1614
- "visible": True,
1615
- "loaded": True,
1616
- "count": len(associated_items),
1617
- "items": associated_items,
1618
- }
1804
+ node_info = self._select_task_node(audit_infos, workflow_node_id, app_key=app_key, record_id=record_id)
1805
+ except QingflowApiError as exc:
1806
+ if not _is_optional_task_audit_info_error(exc):
1807
+ raise
1808
+ detail = self.backend.request(
1809
+ "GET",
1810
+ context,
1811
+ f"/app/{app_key}/apply/{record_id}",
1812
+ params={"role": role, "listType": list_type, "auditNodeId": workflow_node_id},
1813
+ )
1814
+ node_info = self._fallback_task_node_from_detail(
1815
+ detail,
1816
+ workflow_node_id=workflow_node_id,
1817
+ source_error=exc,
1818
+ )
1819
+ context_warnings.append(
1820
+ {
1821
+ "code": "TASK_AUDIT_INFO_UNAVAILABLE",
1822
+ "message": "Task context used the current task detail because auditInfo was unavailable in this permission context.",
1823
+ "backend_code": exc.backend_code,
1824
+ "request_id": exc.request_id,
1825
+ "http_status": exc.http_status,
1826
+ }
1827
+ )
1828
+ if detail is None:
1829
+ detail = self.backend.request(
1830
+ "GET",
1831
+ context,
1832
+ f"/app/{app_key}/apply/{record_id}",
1833
+ params={"role": role, "listType": list_type, "auditNodeId": workflow_node_id},
1834
+ )
1835
+ app_name = self._task_app_name(context=context, app_key=app_key, detail=detail, node_info=node_info)
1836
+ associated_report_visible = self._resolve_associated_report_visible(node_info, detail)
1837
+ associated_reports = {"visible": associated_report_visible, "loaded": False, "count": 0, "items": [], "warnings": []}
1838
+ if include_associated_reports and associated_report_visible:
1839
+ try:
1840
+ asos_chart_list = self.backend.request(
1841
+ "GET",
1842
+ context,
1843
+ f"/app/{app_key}/asosChart",
1844
+ params={"role": role, "auditNodeId": workflow_node_id, "beingDraft": False},
1845
+ )
1846
+ associated_items = [
1847
+ self._normalize_associated_report(item)
1848
+ for item in (asos_chart_list.get("asosCharts") or [])
1849
+ if isinstance(item, dict)
1850
+ ]
1851
+ associated_reports = {
1852
+ "visible": True,
1853
+ "loaded": True,
1854
+ "count": len(associated_items),
1855
+ "items": associated_items,
1856
+ "warnings": [],
1857
+ }
1858
+ except QingflowApiError as exc:
1859
+ if not _is_task_optional_read_error(exc):
1860
+ raise
1861
+ associated_reports = {
1862
+ "visible": True,
1863
+ "loaded": False,
1864
+ "count": 0,
1865
+ "items": [],
1866
+ "warnings": [
1867
+ {
1868
+ "code": "TASK_ASSOCIATED_REPORTS_UNAVAILABLE",
1869
+ "message": "Associated reports are not readable in this permission context; task detail remains available.",
1870
+ "backend_code": exc.backend_code,
1871
+ "request_id": exc.request_id,
1872
+ "http_status": exc.http_status,
1873
+ }
1874
+ ],
1875
+ }
1619
1876
  rollback_items: list[dict[str, Any]] = []
1877
+ rollback_warnings: list[JSONObject] = []
1620
1878
  transfer_items: list[dict[str, Any]] = []
1621
1879
  transfer_warnings: list[JSONObject] = []
1622
1880
  transfer_pagination: JSONObject = {
@@ -1628,13 +1886,26 @@ class TaskContextTools(ToolBase):
1628
1886
  "truncated": False,
1629
1887
  }
1630
1888
  if include_candidates:
1631
- rollback_result = self.backend.request(
1632
- "GET",
1633
- context,
1634
- f"/app/{app_key}/apply/{record_id}/revertNode",
1635
- params={"auditNodeId": workflow_node_id},
1636
- )
1637
- rollback_items = self._rollback_candidate_items(rollback_result)
1889
+ try:
1890
+ rollback_result = self.backend.request(
1891
+ "GET",
1892
+ context,
1893
+ f"/app/{app_key}/apply/{record_id}/revertNode",
1894
+ params={"auditNodeId": workflow_node_id},
1895
+ )
1896
+ rollback_items = self._rollback_candidate_items(rollback_result)
1897
+ except QingflowApiError as exc:
1898
+ if not _is_task_optional_read_error(exc):
1899
+ raise
1900
+ rollback_warnings.append(
1901
+ {
1902
+ "code": "TASK_ROLLBACK_CANDIDATES_UNAVAILABLE",
1903
+ "message": "Rollback candidates are not readable in this permission context; task detail remains available.",
1904
+ "backend_code": exc.backend_code,
1905
+ "request_id": exc.request_id,
1906
+ "http_status": exc.http_status,
1907
+ }
1908
+ )
1638
1909
  transfer_items, transfer_warnings, transfer_pagination = self._transfer_candidate_items(
1639
1910
  context,
1640
1911
  app_key=app_key,
@@ -1657,6 +1928,7 @@ class TaskContextTools(ToolBase):
1657
1928
  context,
1658
1929
  app_key=app_key,
1659
1930
  workflow_node_id=workflow_node_id,
1931
+ node_info=node_info,
1660
1932
  )
1661
1933
  capabilities = self._build_capabilities(
1662
1934
  node_info,
@@ -1703,7 +1975,9 @@ class TaskContextTools(ToolBase):
1703
1975
  "transfer_members": transfer_items,
1704
1976
  "loaded": include_candidates,
1705
1977
  "transfer_pagination": transfer_pagination,
1706
- "warnings": transfer_warnings,
1978
+ "rollback_warnings": rollback_warnings,
1979
+ "transfer_warnings": transfer_warnings,
1980
+ "warnings": [*rollback_warnings, *transfer_warnings],
1707
1981
  },
1708
1982
  "workflow_log_summary": {
1709
1983
  "visible": visibility["audit_record_visible"],
@@ -1712,6 +1986,7 @@ class TaskContextTools(ToolBase):
1712
1986
  "qrobot_log_visible": visibility["qrobot_record_visible"],
1713
1987
  },
1714
1988
  "update_schema": update_schema,
1989
+ "warnings": context_warnings,
1715
1990
  }
1716
1991
 
1717
1992
  def _compact_task_get_context(self, data: dict[str, Any]) -> dict[str, Any]:
@@ -1781,12 +2056,14 @@ class TaskContextTools(ToolBase):
1781
2056
  "loaded": bool(associated_reports.get("loaded")),
1782
2057
  "count": len(associated_items),
1783
2058
  "items": associated_items,
2059
+ "warnings": associated_reports.get("warnings") or [],
1784
2060
  },
1785
2061
  "rollback_candidates": {
1786
2062
  "available": "rollback" in available_actions,
1787
2063
  "loaded": bool(candidates.get("loaded")),
1788
2064
  "count": len(rollback_items),
1789
2065
  "items": rollback_items,
2066
+ "warnings": candidates.get("rollback_warnings") or [],
1790
2067
  },
1791
2068
  "transfer_candidates": {
1792
2069
  "available": "transfer" in available_actions,
@@ -1794,7 +2071,7 @@ class TaskContextTools(ToolBase):
1794
2071
  "count": len(transfer_items),
1795
2072
  "items": transfer_items,
1796
2073
  "pagination": transfer_pagination,
1797
- "warnings": candidates.get("warnings") or [],
2074
+ "warnings": candidates.get("transfer_warnings") or [],
1798
2075
  },
1799
2076
  },
1800
2077
  }
@@ -1829,6 +2106,8 @@ class TaskContextTools(ToolBase):
1829
2106
  metadata["blockers"] = blockers
1830
2107
  if warnings:
1831
2108
  metadata["warnings"] = warnings
2109
+ if update_schema.get("editable_question_ids_source"):
2110
+ metadata["editable_question_ids_source"] = update_schema.get("editable_question_ids_source")
1832
2111
  return metadata
1833
2112
 
1834
2113
  def _compact_initiator(self, payload: Any) -> dict[str, Any] | None:
@@ -1877,7 +2156,9 @@ class TaskContextTools(ToolBase):
1877
2156
  ) -> str | None:
1878
2157
  try:
1879
2158
  base_info = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
1880
- except QingflowApiError:
2159
+ except QingflowApiError as exc:
2160
+ if not _is_task_optional_read_error(exc):
2161
+ raise
1881
2162
  return None
1882
2163
  if not isinstance(base_info, dict):
1883
2164
  return None
@@ -1895,7 +2176,9 @@ class TaskContextTools(ToolBase):
1895
2176
  ) -> str | None:
1896
2177
  try:
1897
2178
  visible_apps = self.backend.request("GET", context, "/tag/apps")
1898
- except QingflowApiError:
2179
+ except QingflowApiError as exc:
2180
+ if not _is_task_optional_read_error(exc):
2181
+ raise
1899
2182
  return None
1900
2183
  return self._find_task_app_name_in_visible_apps(visible_apps, app_key=app_key)
1901
2184
 
@@ -2095,6 +2378,54 @@ class TaskContextTools(ToolBase):
2095
2378
  )
2096
2379
  )
2097
2380
 
2381
+ def _fallback_task_node_from_detail(
2382
+ self,
2383
+ detail: dict[str, Any],
2384
+ *,
2385
+ workflow_node_id: int,
2386
+ source_error: QingflowApiError,
2387
+ ) -> dict[str, Any]:
2388
+ """Build a conservative task node snapshot from the readable task detail."""
2389
+ node_info: dict[str, Any] = {
2390
+ "auditNodeId": workflow_node_id,
2391
+ "nodeId": workflow_node_id,
2392
+ "auditNodeName": (
2393
+ detail.get("auditNodeName")
2394
+ or detail.get("nodeName")
2395
+ or detail.get("workflowNodeName")
2396
+ or detail.get("currentNodeName")
2397
+ ),
2398
+ "_auditInfoUnavailable": True,
2399
+ "_auditInfoError": {
2400
+ "http_status": source_error.http_status,
2401
+ "backend_code": source_error.backend_code,
2402
+ "category": source_error.category,
2403
+ },
2404
+ }
2405
+ for key in (
2406
+ "canTransfer",
2407
+ "canRevert",
2408
+ "canUrge",
2409
+ "rejectBtnStatus",
2410
+ "canRevoke",
2411
+ "beingEndWorkflow",
2412
+ "beingCanApplyAgain",
2413
+ "beingSubmitCheck",
2414
+ "beingSubmitPreview",
2415
+ "feedbackRequiredOperationType",
2416
+ "queAuthSetting",
2417
+ "auditRecordVisible",
2418
+ "qrobotRecordBeingVisible",
2419
+ "beingWorkflowNodeFutureListVisible",
2420
+ "commentStatus",
2421
+ "asosChartVisible",
2422
+ ):
2423
+ if key in detail:
2424
+ node_info[key] = detail[key]
2425
+ if "viewAsosChartVisible" in detail and "asosChartVisible" not in node_info:
2426
+ node_info["asosChartVisible"] = detail.get("viewAsosChartVisible")
2427
+ return node_info
2428
+
2098
2429
  def _build_capabilities(
2099
2430
  self,
2100
2431
  node_info: dict[str, Any],
@@ -2159,35 +2490,18 @@ class TaskContextTools(ToolBase):
2159
2490
  try:
2160
2491
  app_schema = self._record_tools._get_form_schema(profile, context, app_key, force_refresh=False)
2161
2492
  except QingflowApiError as error:
2162
- public_schema: JSONObject = {
2163
- "schema_scope": "task_update_ready",
2164
- "writable_fields": [],
2165
- "payload_template": {},
2166
- "blockers": ["TASK_UPDATE_SCHEMA_UNAVAILABLE"],
2167
- "warnings": [
2168
- {
2169
- "code": "TASK_UPDATE_SCHEMA_UNAVAILABLE",
2170
- "message": "task detail could not load the form schema for the current app, so node-scoped update schema is unavailable.",
2171
- }
2172
- ],
2173
- "selection": {
2174
- "app_key": app_key,
2175
- "record_id": record_id_text,
2176
- "workflow_node_id": workflow_node_id,
2177
- },
2178
- "transport_error": {
2179
- "http_status": error.http_status,
2180
- "backend_code": error.backend_code,
2181
- "category": error.category,
2182
- },
2183
- }
2184
- return {
2185
- "public_schema": public_schema,
2186
- "index": None,
2187
- "editable_question_ids": [],
2188
- "effective_editable_question_ids": [],
2189
- "editable_question_ids_source": "schema_unavailable",
2190
- }
2493
+ if not _is_task_optional_read_error(error):
2494
+ raise
2495
+ return self._build_task_update_schema_from_runtime_answers(
2496
+ profile=profile,
2497
+ context=context,
2498
+ app_key=app_key,
2499
+ record_id=record_id,
2500
+ workflow_node_id=workflow_node_id,
2501
+ node_info=node_info,
2502
+ current_answers=current_answers,
2503
+ source_error=error,
2504
+ )
2191
2505
 
2192
2506
  question_relations = _collect_question_relations(app_schema)
2193
2507
  linked_field_ids = _collect_linked_required_field_ids(question_relations)
@@ -2260,6 +2574,7 @@ class TaskContextTools(ToolBase):
2260
2574
  "record_id": record_id_text,
2261
2575
  "workflow_node_id": workflow_node_id,
2262
2576
  },
2577
+ "editable_question_ids_source": source,
2263
2578
  }
2264
2579
  return {
2265
2580
  "public_schema": public_schema,
@@ -2269,6 +2584,96 @@ class TaskContextTools(ToolBase):
2269
2584
  "editable_question_ids_source": source,
2270
2585
  }
2271
2586
 
2587
+ def _build_task_update_schema_from_runtime_answers(
2588
+ self,
2589
+ *,
2590
+ profile: str,
2591
+ context: BackendRequestContext,
2592
+ app_key: str,
2593
+ record_id: int,
2594
+ workflow_node_id: int,
2595
+ node_info: dict[str, Any],
2596
+ current_answers: Any,
2597
+ source_error: QingflowApiError,
2598
+ ) -> dict[str, Any]:
2599
+ """Build node-scoped edit schema from the task detail when app schema is unavailable."""
2600
+ record_id_text = stringify_backend_id(record_id)
2601
+ editable_question_ids, schema_warnings, source = self._resolve_task_editable_question_ids(
2602
+ context,
2603
+ app_key=app_key,
2604
+ workflow_node_id=workflow_node_id,
2605
+ node_info=node_info,
2606
+ )
2607
+ schema_warnings.insert(
2608
+ 0,
2609
+ {
2610
+ "code": "TASK_UPDATE_SCHEMA_APP_SCHEMA_UNAVAILABLE",
2611
+ "message": "task update schema used current task answers because the app applicant schema was unavailable in this permission context.",
2612
+ "transport_error": {
2613
+ "http_status": source_error.http_status,
2614
+ "backend_code": source_error.backend_code,
2615
+ "category": source_error.category,
2616
+ },
2617
+ },
2618
+ )
2619
+ index = _build_answer_backed_field_index(
2620
+ current_answers,
2621
+ field_id_filter={str(que_id) for que_id in editable_question_ids if que_id > 0},
2622
+ )
2623
+ effective_editable_ids = set(editable_question_ids)
2624
+ writable_fields: list[JSONObject] = []
2625
+ for field in index.by_id.values():
2626
+ if field.que_type in LAYOUT_ONLY_QUE_TYPES or field.que_id not in effective_editable_ids:
2627
+ continue
2628
+ editable_field = _clone_form_field(field, readonly=False)
2629
+ write_hints = self._record_tools._schema_write_hints(editable_field)
2630
+ if not bool(write_hints.get("writable")):
2631
+ continue
2632
+ writable_field = self._record_tools._ready_schema_field_payload(
2633
+ profile,
2634
+ context,
2635
+ editable_field,
2636
+ ws_id=context.ws_id,
2637
+ required_override=False,
2638
+ linkage_payloads_by_field_id={},
2639
+ )
2640
+ writable_field.setdefault("field_id", editable_field.que_id)
2641
+ writable_fields.append(writable_field)
2642
+
2643
+ blockers: list[str] = []
2644
+ if not writable_fields:
2645
+ blockers.append("NO_TASK_EDITABLE_FIELDS")
2646
+ schema_warnings.append(
2647
+ {
2648
+ "code": "NO_TASK_EDITABLE_FIELDS",
2649
+ "message": "the current task node does not expose any writable fields in the current task detail.",
2650
+ }
2651
+ )
2652
+ public_schema: JSONObject = {
2653
+ "schema_scope": "task_update_ready",
2654
+ "writable_fields": writable_fields,
2655
+ "payload_template": {
2656
+ item["title"]: self._record_tools._ready_schema_template_value(item)
2657
+ for item in writable_fields
2658
+ if isinstance(item, dict) and item.get("title")
2659
+ },
2660
+ "blockers": blockers,
2661
+ "warnings": schema_warnings,
2662
+ "selection": {
2663
+ "app_key": app_key,
2664
+ "record_id": record_id_text,
2665
+ "workflow_node_id": workflow_node_id,
2666
+ },
2667
+ "editable_question_ids_source": f"{source}_runtime_answers",
2668
+ }
2669
+ return {
2670
+ "public_schema": public_schema,
2671
+ "index": index,
2672
+ "editable_question_ids": sorted(editable_question_ids),
2673
+ "effective_editable_question_ids": sorted(effective_editable_ids),
2674
+ "editable_question_ids_source": f"{source}_runtime_answers",
2675
+ }
2676
+
2272
2677
  def _augment_task_editable_field_index(
2273
2678
  self,
2274
2679
  *,
@@ -2328,12 +2733,15 @@ class TaskContextTools(ToolBase):
2328
2733
  if question_ids:
2329
2734
  return question_ids, warnings, "workflow_editable_que_ids"
2330
2735
  except QingflowApiError as error:
2331
- if error.backend_code not in {40002, 40027, 404} and error.http_status != 404:
2736
+ if not _is_task_optional_read_error(error):
2332
2737
  raise
2333
2738
  warnings.append(
2334
2739
  {
2335
2740
  "code": "TASK_EDITABLE_IDS_FALLBACK",
2336
2741
  "message": "editable question ids endpoint is unavailable in the current route; task update schema fell back to queAuthSetting and may be conservative.",
2742
+ "backend_code": error.backend_code,
2743
+ "http_status": error.http_status,
2744
+ "request_id": error.request_id,
2337
2745
  }
2338
2746
  )
2339
2747
  fallback_ids = self._editable_ids_from_que_auth_setting(node_info.get("queAuthSetting"))
@@ -2345,6 +2753,7 @@ class TaskContextTools(ToolBase):
2345
2753
  *,
2346
2754
  app_key: str,
2347
2755
  workflow_node_id: int,
2756
+ node_info: dict[str, Any],
2348
2757
  ) -> tuple[bool, list[JSONObject], str]:
2349
2758
  """执行内部辅助逻辑。"""
2350
2759
  try:
@@ -2354,15 +2763,28 @@ class TaskContextTools(ToolBase):
2354
2763
  f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
2355
2764
  )
2356
2765
  except QingflowApiError as error:
2766
+ if not _is_task_optional_read_error(error):
2767
+ raise
2768
+ fallback_ids = self._editable_ids_from_que_auth_setting(node_info.get("queAuthSetting"))
2769
+ fallback_source = "que_auth_setting" if fallback_ids else "backend_editable_que_ids_unavailable"
2357
2770
  warning: JSONObject = {
2358
- "code": "TASK_SAVE_ONLY_SIGNAL_UNAVAILABLE",
2359
- "message": "save_only is hidden because backend editableQueIds is unavailable for the current node; MCP no longer infers save_only from local schema reconstruction.",
2771
+ "code": "TASK_SAVE_ONLY_EDITABLE_IDS_FALLBACK" if fallback_ids else "TASK_SAVE_ONLY_SIGNAL_UNAVAILABLE",
2772
+ "message": (
2773
+ "save_only availability used current task queAuthSetting because backend editableQueIds was unavailable in this permission context."
2774
+ if fallback_ids
2775
+ else "save_only is hidden because backend editableQueIds is unavailable and current task detail does not expose editable fields."
2776
+ ),
2360
2777
  }
2361
2778
  if error.backend_code is not None:
2362
2779
  warning["backend_code"] = error.backend_code
2363
2780
  if error.http_status is not None:
2364
2781
  warning["http_status"] = error.http_status
2365
- return False, [warning], "backend_editable_que_ids_unavailable"
2782
+ if error.request_id is not None:
2783
+ warning["request_id"] = error.request_id
2784
+ if fallback_ids:
2785
+ warning["field_ids"] = sorted(fallback_ids)
2786
+ return True, [warning], fallback_source
2787
+ return False, [warning], fallback_source
2366
2788
  return bool(self._extract_question_ids(payload)), [], "workflow_editable_que_ids"
2367
2789
 
2368
2790
  def _extract_question_ids(self, payload: Any) -> set[int]:
@@ -2655,12 +3077,33 @@ class TaskContextTools(ToolBase):
2655
3077
  warnings: list[JSONObject] = []
2656
3078
 
2657
3079
  while page_num <= max_pages:
2658
- result = self.backend.request(
2659
- "GET",
2660
- context,
2661
- f"/app/{app_key}/apply/{record_id}/transfer/member",
2662
- params={"pageNum": page_num, "pageSize": page_size, "auditNodeId": workflow_node_id},
2663
- )
3080
+ try:
3081
+ result = self.backend.request(
3082
+ "GET",
3083
+ context,
3084
+ f"/app/{app_key}/apply/{record_id}/transfer/member",
3085
+ params={"pageNum": page_num, "pageSize": page_size, "auditNodeId": workflow_node_id},
3086
+ )
3087
+ except QingflowApiError as exc:
3088
+ if not _is_task_optional_read_error(exc):
3089
+ raise
3090
+ warnings.append(
3091
+ {
3092
+ "code": "TASK_TRANSFER_CANDIDATES_UNAVAILABLE",
3093
+ "message": "Transfer candidates are not readable in this permission context; task detail remains available.",
3094
+ "backend_code": exc.backend_code,
3095
+ "request_id": exc.request_id,
3096
+ "http_status": exc.http_status,
3097
+ }
3098
+ )
3099
+ return items, warnings, {
3100
+ "loaded": False,
3101
+ "page_size": page_size,
3102
+ "fetched_pages": fetched_pages,
3103
+ "reported_total": reported_total,
3104
+ "page_amount": page_amount,
3105
+ "truncated": False,
3106
+ }
2664
3107
  fetched_pages += 1
2665
3108
  raw_items = _approval_page_items(result)
2666
3109
  fetched_raw_count += len(raw_items)
@@ -2825,7 +3268,9 @@ class TaskContextTools(ToolBase):
2825
3268
  context,
2826
3269
  f"/chart/{chart_key}/auth",
2827
3270
  )
2828
- except QingflowApiError:
3271
+ except QingflowApiError as error:
3272
+ if not _is_task_optional_read_error(error):
3273
+ raise
2829
3274
  return False
2830
3275
  if not isinstance(auth, dict):
2831
3276
  return False
@@ -3002,3 +3447,62 @@ class TaskContextTools(ToolBase):
3002
3447
  "qf_version": context.qf_version,
3003
3448
  "qf_version_source": context.qf_version_source or ("context" if context.qf_version else "unknown"),
3004
3449
  }
3450
+
3451
+
3452
+ def _is_optional_task_audit_info_error(error: QingflowApiError) -> bool:
3453
+ """Whether task auditInfo may be replaced by current task detail."""
3454
+ if is_auth_like_error(error):
3455
+ return False
3456
+ return _is_task_permission_context_error(error) or error.http_status == 404
3457
+
3458
+
3459
+ def _task_box_record_list_type(task_box: str | None) -> int:
3460
+ normalized = str(task_box or "todo").strip().lower()
3461
+ if normalized == "initiated":
3462
+ return 14
3463
+ if normalized == "done":
3464
+ return 2
3465
+ if normalized == "cc":
3466
+ return 12
3467
+ return 1
3468
+
3469
+
3470
+ def _task_box_record_role(task_box: str | None) -> int:
3471
+ normalized = str(task_box or "todo").strip().lower()
3472
+ if normalized == "initiated":
3473
+ return 2
3474
+ return 3
3475
+
3476
+
3477
+ def _is_optional_task_box_locator_error(error: QingflowApiError) -> bool:
3478
+ """Whether one task box may be skipped while resolving a task_id."""
3479
+ if is_auth_like_error(error):
3480
+ return False
3481
+ return _is_task_permission_context_error(error) or error.http_status == 404
3482
+
3483
+
3484
+ def _is_task_optional_read_error(error: QingflowApiError) -> bool:
3485
+ if is_auth_like_error(error):
3486
+ return False
3487
+ backend_code = _task_backend_code(error)
3488
+ return backend_code in {40002, 40027, 404} or error.http_status == 404
3489
+
3490
+
3491
+ def _is_task_permission_context_error(error: QingflowApiError) -> bool:
3492
+ if is_auth_like_error(error):
3493
+ return False
3494
+ return _task_backend_code(error) in {40002, 40027}
3495
+
3496
+
3497
+ def _is_permission_context_error_payload(payload: dict[str, Any]) -> bool:
3498
+ if str(payload.get("category") or "").strip().lower() == "auth":
3499
+ return False
3500
+ if message_looks_like_invalid_token(payload.get("message")):
3501
+ return False
3502
+ if backend_code_value_int(payload.get("http_status")) == 401:
3503
+ return False
3504
+ return backend_code_value_int(payload.get("backend_code")) in {40002, 40027}
3505
+
3506
+
3507
+ def _task_backend_code(error: QingflowApiError) -> int | None:
3508
+ return backend_code_int(error)