@josephyan/qingflow-app-builder-mcp 1.1.24 → 1.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +5 -6
  2. package/docs/local-agent-install.md +6 -57
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
  5. package/npm/lib/runtime.mjs +101 -21
  6. package/npm/scripts/postinstall.mjs +10 -1
  7. package/package.json +2 -3
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +42 -104
  10. package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
  11. package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
  12. package/skills/qingflow-app-builder/references/create-app.md +11 -30
  13. package/skills/qingflow-app-builder/references/environments.md +1 -1
  14. package/skills/qingflow-app-builder/references/gotchas.md +9 -31
  15. package/skills/qingflow-app-builder/references/match-rules.md +0 -15
  16. package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
  17. package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
  18. package/skills/qingflow-app-builder/references/update-flow.md +38 -22
  19. package/skills/qingflow-app-builder/references/update-schema.md +0 -14
  20. package/skills/qingflow-app-builder/references/update-views.md +14 -83
  21. package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
  22. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
  23. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
  24. package/skills/qingflow-mcp-setup/SKILL.md +111 -0
  25. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  26. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  27. package/skills/qingflow-mcp-setup/references/environments.md +61 -0
  28. package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
  29. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  30. package/skills/qingflow-workflow-builder/SKILL.md +96 -0
  31. package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
  32. package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
  33. package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
  34. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
  35. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
  36. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
  37. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
  38. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
  39. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
  40. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
  41. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
  42. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
  43. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
  44. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
  45. package/src/qingflow_mcp/__init__.py +1 -1
  46. package/src/qingflow_mcp/__main__.py +2 -6
  47. package/src/qingflow_mcp/builder_facade/models.py +108 -485
  48. package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
  49. package/src/qingflow_mcp/cli/commands/builder.py +364 -322
  50. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  51. package/src/qingflow_mcp/cli/commands/common.py +3 -12
  52. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  53. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  54. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  55. package/src/qingflow_mcp/cli/commands/record.py +27 -101
  56. package/src/qingflow_mcp/cli/commands/task.py +47 -28
  57. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  58. package/src/qingflow_mcp/cli/context.py +3 -0
  59. package/src/qingflow_mcp/cli/formatters.py +16 -784
  60. package/src/qingflow_mcp/cli/main.py +33 -117
  61. package/src/qingflow_mcp/errors.py +2 -43
  62. package/src/qingflow_mcp/public_surface.py +17 -26
  63. package/src/qingflow_mcp/response_trim.py +17 -81
  64. package/src/qingflow_mcp/server.py +12 -14
  65. package/src/qingflow_mcp/server_app_builder.py +36 -79
  66. package/src/qingflow_mcp/server_app_user.py +16 -22
  67. package/src/qingflow_mcp/session_store.py +7 -11
  68. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  69. package/src/qingflow_mcp/solution/executor.py +18 -245
  70. package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3100
  71. package/src/qingflow_mcp/tools/app_tools.py +43 -184
  72. package/src/qingflow_mcp/tools/approval_tools.py +35 -197
  73. package/src/qingflow_mcp/tools/auth_tools.py +16 -92
  74. package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
  75. package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
  76. package/src/qingflow_mcp/tools/directory_tools.py +72 -236
  77. package/src/qingflow_mcp/tools/export_tools.py +34 -244
  78. package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
  79. package/src/qingflow_mcp/tools/file_tools.py +3 -9
  80. package/src/qingflow_mcp/tools/import_tools.py +49 -336
  81. package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
  82. package/src/qingflow_mcp/tools/package_tools.py +6 -118
  83. package/src/qingflow_mcp/tools/portal_tools.py +3 -39
  84. package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
  85. package/src/qingflow_mcp/tools/record_tools.py +360 -1145
  86. package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
  87. package/src/qingflow_mcp/tools/role_tools.py +9 -80
  88. package/src/qingflow_mcp/tools/solution_tools.py +45 -59
  89. package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
  90. package/src/qingflow_mcp/tools/task_tools.py +29 -113
  91. package/src/qingflow_mcp/tools/view_tools.py +3 -106
  92. package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
  93. package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
  94. package/npm/bin/qingflow-skills.mjs +0 -5
  95. package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
  96. package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
  97. package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
  98. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -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, backend_code_int, backend_code_value_int, is_auth_like_error, message_looks_like_invalid_token, raise_tool_error
12
+ from ..errors import QingflowApiError, 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, _should_retry_asos_data
17
+ from .qingbi_report_tools import _qingbi_base_url
18
18
  from .record_tools import (
19
19
  FieldIndex,
20
20
  LAYOUT_ONLY_QUE_TYPES,
@@ -38,9 +38,6 @@ 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
-
44
41
  class TaskContextTools(ToolBase):
45
42
  """任务上下文工具(中文名:任务上下文与审批执行)。
46
43
 
@@ -88,37 +85,33 @@ class TaskContextTools(ToolBase):
88
85
  page_size=page_size,
89
86
  )
90
87
 
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
- )
88
+ @mcp.tool()
97
89
  def task_get(
98
90
  profile: str = DEFAULT_PROFILE,
99
91
  task_id: str = "",
92
+ app_key: str = "",
93
+ record_id: str = "",
94
+ workflow_node_id: int = 0,
100
95
  include_candidates: bool = True,
101
96
  include_associated_reports: bool = True,
102
97
  ) -> dict[str, Any]:
103
98
  return self.task_get(
104
99
  profile=profile,
105
100
  task_id=task_id,
106
- app_key="",
107
- record_id="",
108
- workflow_node_id=0,
101
+ app_key=app_key,
102
+ record_id=record_id,
103
+ workflow_node_id=workflow_node_id,
109
104
  include_candidates=include_candidates,
110
105
  include_associated_reports=include_associated_reports,
111
106
  )
112
107
 
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
- )
108
+ @mcp.tool(description=self._high_risk_tool_description(operation="execute", target="workflow task action"))
119
109
  def task_action_execute(
120
110
  profile: str = DEFAULT_PROFILE,
121
111
  task_id: str = "",
112
+ app_key: str = "",
113
+ record_id: str = "",
114
+ workflow_node_id: int = 0,
122
115
  action: str = "",
123
116
  payload: dict[str, Any] | None = None,
124
117
  fields: dict[str, Any] | None = None,
@@ -126,20 +119,21 @@ class TaskContextTools(ToolBase):
126
119
  return self.task_action_execute(
127
120
  profile=profile,
128
121
  task_id=task_id,
122
+ app_key=app_key,
123
+ record_id=record_id,
124
+ workflow_node_id=workflow_node_id,
129
125
  action=action,
130
126
  payload=payload or {},
131
127
  fields=fields or {},
132
128
  )
133
129
 
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
- )
130
+ @mcp.tool()
140
131
  def task_associated_report_detail_get(
141
132
  profile: str = DEFAULT_PROFILE,
142
133
  task_id: str = "",
134
+ app_key: str = "",
135
+ record_id: str = "",
136
+ workflow_node_id: int = 0,
143
137
  report_id: int = 0,
144
138
  page: int = 1,
145
139
  page_size: int = 20,
@@ -147,30 +141,28 @@ class TaskContextTools(ToolBase):
147
141
  return self.task_associated_report_detail_get(
148
142
  profile=profile,
149
143
  task_id=task_id,
150
- app_key="",
151
- record_id="",
152
- workflow_node_id=0,
144
+ app_key=app_key,
145
+ record_id=record_id,
146
+ workflow_node_id=workflow_node_id,
153
147
  report_id=report_id,
154
148
  page=page,
155
149
  page_size=page_size,
156
150
  )
157
151
 
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
- )
152
+ @mcp.tool()
164
153
  def task_workflow_log_get(
165
154
  profile: str = DEFAULT_PROFILE,
166
155
  task_id: str = "",
156
+ app_key: str = "",
157
+ record_id: str = "",
158
+ workflow_node_id: int = 0,
167
159
  ) -> dict[str, Any]:
168
160
  return self.task_workflow_log_get(
169
161
  profile=profile,
170
162
  task_id=task_id,
171
- app_key="",
172
- record_id="",
173
- workflow_node_id=0,
163
+ app_key=app_key,
164
+ record_id=record_id,
165
+ workflow_node_id=workflow_node_id,
174
166
  )
175
167
 
176
168
  @tool_cn_name("任务上下文列表")
@@ -279,7 +271,6 @@ class TaskContextTools(ToolBase):
279
271
  resolved_app_key = str(locator["app_key"])
280
272
  resolved_record_id = int(locator["record_id"])
281
273
  resolved_workflow_node_id = int(locator["workflow_node_id"])
282
- resolved_task_box = str(locator.get("task_box") or "todo")
283
274
  self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
284
275
  data = self._build_task_context(
285
276
  profile=profile,
@@ -287,12 +278,10 @@ class TaskContextTools(ToolBase):
287
278
  app_key=resolved_app_key,
288
279
  record_id=resolved_record_id,
289
280
  workflow_node_id=resolved_workflow_node_id,
290
- task_box=resolved_task_box,
291
281
  include_candidates=include_candidates,
292
282
  include_associated_reports=include_associated_reports,
293
283
  current_uid=session_profile.uid,
294
284
  )
295
- context_warnings = data.get("warnings") if isinstance(data.get("warnings"), list) else []
296
285
  data = self._compact_task_get_context(data)
297
286
  task_payload = data.get("task")
298
287
  if isinstance(task_payload, dict) and task_id_text is not None:
@@ -302,7 +291,7 @@ class TaskContextTools(ToolBase):
302
291
  "ws_id": session_profile.selected_ws_id,
303
292
  "ok": True,
304
293
  "request_route": self._request_route_payload(context),
305
- "warnings": context_warnings,
294
+ "warnings": [],
306
295
  "output_profile": "normal",
307
296
  "data": data,
308
297
  }
@@ -314,7 +303,9 @@ class TaskContextTools(ToolBase):
314
303
  self,
315
304
  *,
316
305
  profile: str,
317
- task_id: Any,
306
+ app_key: str,
307
+ record_id: Any,
308
+ workflow_node_id: int,
318
309
  fields: dict[str, Any] | None = None,
319
310
  ) -> dict[str, Any]:
320
311
  """执行任务相关逻辑。"""
@@ -323,7 +314,9 @@ class TaskContextTools(ToolBase):
323
314
  raise_tool_error(QingflowApiError.config_error("fields is required and must be non-empty for task_save_only"))
324
315
  return self.task_action_execute(
325
316
  profile=profile,
326
- task_id=task_id,
317
+ app_key=app_key,
318
+ record_id=record_id,
319
+ workflow_node_id=workflow_node_id,
327
320
  action="save_only",
328
321
  payload={},
329
322
  fields=field_updates,
@@ -331,34 +324,6 @@ class TaskContextTools(ToolBase):
331
324
 
332
325
  @tool_cn_name("执行任务动作")
333
326
  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(
362
327
  self,
363
328
  *,
364
329
  profile: str,
@@ -370,8 +335,10 @@ class TaskContextTools(ToolBase):
370
335
  payload: dict[str, Any],
371
336
  fields: dict[str, Any] | None = None,
372
337
  ) -> dict[str, Any]:
338
+ """执行任务相关逻辑。"""
373
339
  if task_id in (None, ""):
374
340
  normalize_positive_id_int(record_id, field_name="record_id")
341
+
375
342
  normalized_action = (action or "").strip().lower()
376
343
  if normalized_action not in {"approve", "reject", "rollback", "transfer", "urge", "save_only"}:
377
344
  raise_tool_error(
@@ -401,63 +368,8 @@ class TaskContextTools(ToolBase):
401
368
  resolved_record_id = int(locator["record_id"])
402
369
  resolved_record_id_text = str(locator["record_id_text"] or "")
403
370
  resolved_workflow_node_id = int(locator["workflow_node_id"])
404
- resolved_task_box = str(locator.get("task_box") or "todo")
405
371
  record_id_text = resolved_record_id_text
406
372
  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
- )
461
373
  try:
462
374
  task_context = self._build_task_context(
463
375
  profile=profile,
@@ -465,13 +377,12 @@ class TaskContextTools(ToolBase):
465
377
  app_key=resolved_app_key,
466
378
  record_id=resolved_record_id,
467
379
  workflow_node_id=resolved_workflow_node_id,
468
- task_box=resolved_task_box,
469
380
  include_candidates=False,
470
381
  include_associated_reports=False,
471
382
  current_uid=session_profile.uid,
472
383
  )
473
384
  except QingflowApiError as error:
474
- if backend_code_int(error) == 46001:
385
+ if error.backend_code == 46001:
475
386
  return self._task_action_visibility_unverified_response(
476
387
  profile=profile,
477
388
  session_profile=session_profile,
@@ -511,7 +422,7 @@ class TaskContextTools(ToolBase):
511
422
  raise_tool_error(QingflowApiError.config_error(message))
512
423
  raise_tool_error(
513
424
  QingflowApiError.config_error(
514
- f"task action '{normalized_action}' is not currently available for task_id='{task_id_text}'"
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}"
515
426
  )
516
427
  )
517
428
  feedback_required_for = capabilities.get("action_constraints", {}).get("feedback_required_for") or []
@@ -559,7 +470,7 @@ class TaskContextTools(ToolBase):
559
470
  prepared_fields=prepared_fields,
560
471
  )
561
472
  except QingflowApiError as error:
562
- if backend_code_int(error) == 46001:
473
+ if error.backend_code == 46001:
563
474
  return self._task_action_visibility_unverified_response(
564
475
  profile=profile,
565
476
  session_profile=session_profile,
@@ -607,8 +518,6 @@ class TaskContextTools(ToolBase):
607
518
  "ok": bool(raw.get("ok", True)) and status != "failed",
608
519
  "status": status,
609
520
  "error_code": error_code,
610
- "action_executed": True,
611
- "safe_to_retry": False,
612
521
  "request_route": raw.get("request_route") or self._request_route_payload(context),
613
522
  "warnings": warnings,
614
523
  "verification": verification,
@@ -771,8 +680,6 @@ class TaskContextTools(ToolBase):
771
680
  "http_status": error.http_status,
772
681
  "backend_code": error.backend_code,
773
682
  "category": error.category,
774
- "request_id": error.request_id,
775
- "message": error.message,
776
683
  }
777
684
 
778
685
  log_items: list[dict[str, Any]] = []
@@ -800,8 +707,6 @@ class TaskContextTools(ToolBase):
800
707
  "http_status": error.http_status,
801
708
  "backend_code": error.backend_code,
802
709
  "category": error.category,
803
- "request_id": error.request_id,
804
- "message": error.message,
805
710
  }
806
711
 
807
712
  todo_items = self._safe_task_list_items(profile=profile, task_box="todo", app_key=app_key)
@@ -851,7 +756,7 @@ class TaskContextTools(ToolBase):
851
756
  runtime_consumed_after_action = bool(
852
757
  runtime_verified
853
758
  and isinstance(record_state_error, dict)
854
- and backend_code_value_int(record_state_error.get("backend_code")) == 46001
759
+ and record_state_error.get("backend_code") == 46001
855
760
  )
856
761
  if runtime_consumed_after_action:
857
762
  verification["record_state_scope"] = "current_node_runtime"
@@ -866,28 +771,6 @@ class TaskContextTools(ToolBase):
866
771
  ),
867
772
  }
868
773
  )
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
- )
891
774
  verification["runtime_continuation_verified"] = runtime_verified
892
775
  if not runtime_verified:
893
776
  warnings.append(
@@ -1048,9 +931,7 @@ class TaskContextTools(ToolBase):
1048
931
  baseline["workflow_log_visible"] = True
1049
932
  baseline["workflow_log_count"] = len(log_items)
1050
933
  baseline["workflow_log_digest"] = self._workflow_log_digest(log_items)
1051
- except QingflowApiError as exc:
1052
- if is_auth_like_error(exc):
1053
- raise
934
+ except QingflowApiError:
1054
935
  pass
1055
936
  todo_items = self._safe_task_list_items(profile=profile, task_box="todo", app_key=app_key)
1056
937
  baseline["downstream_todo_nodes"] = sorted(
@@ -1177,9 +1058,7 @@ class TaskContextTools(ToolBase):
1177
1058
  page=1,
1178
1059
  page_size=50,
1179
1060
  )
1180
- except QingflowApiError as exc:
1181
- if not _is_task_optional_read_error(exc):
1182
- raise
1061
+ except QingflowApiError:
1183
1062
  return []
1184
1063
  items = response.get("items") if isinstance(response, dict) else None
1185
1064
  if not isinstance(items, list):
@@ -1273,36 +1152,21 @@ class TaskContextTools(ToolBase):
1273
1152
  task_id_text = normalize_positive_id_text(task_id, field_name="task_id")
1274
1153
  searched_task_boxes = ("todo", "initiated", "cc", "done")
1275
1154
  incomplete_task_boxes: list[str] = []
1276
- inaccessible_task_boxes: list[dict[str, Any]] = []
1277
- truncated_task_boxes: list[dict[str, Any]] = []
1278
1155
  page_size = 100
1279
1156
  for task_box in searched_task_boxes:
1280
1157
  page = 1
1281
1158
  page_amount: int | None = None
1282
1159
  while True:
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
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
+ )
1306
1170
  items = response.get("items") if isinstance(response.get("items"), list) else []
1307
1171
  for item in items:
1308
1172
  if not isinstance(item, dict) or not ids_equal(item.get("task_id"), task_id_text):
@@ -1328,52 +1192,17 @@ class TaskContextTools(ToolBase):
1328
1192
  break
1329
1193
  if not items or len(items) < page_size:
1330
1194
  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
1341
1195
  page += 1
1342
1196
  if incomplete_task_boxes:
1343
1197
  searched = ", ".join(incomplete_task_boxes)
1344
1198
  raise_tool_error(
1345
1199
  QingflowApiError.config_error(
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
- },
1200
+ f"task_id={task_id_text} resolved to an incomplete task locator in task_box={searched}; please refresh the task list and retry"
1372
1201
  )
1373
1202
  )
1374
1203
  raise_tool_error(
1375
1204
  QingflowApiError.config_error(
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."
1205
+ f"task_id={task_id_text} was not found in the current visible task boxes (todo, initiated, cc, done)"
1377
1206
  )
1378
1207
  )
1379
1208
 
@@ -1395,7 +1224,6 @@ class TaskContextTools(ToolBase):
1395
1224
  resolved_app_key = str(locator["app_key"])
1396
1225
  resolved_record_id = normalize_positive_id_int(locator["record_id"], field_name="record_id")
1397
1226
  resolved_workflow_node_id = int(locator["workflow_node_id"])
1398
- resolved_task_box = str(locator.get("task_box") or "todo")
1399
1227
  explicit_app_key = (app_key or "").strip()
1400
1228
  if explicit_app_key and explicit_app_key != resolved_app_key:
1401
1229
  raise_tool_error(
@@ -1420,10 +1248,8 @@ class TaskContextTools(ToolBase):
1420
1248
  else:
1421
1249
  resolved_record_id = normalize_positive_id_int(record_id, field_name="record_id")
1422
1250
  resolved_workflow_node_id = int(workflow_node_id)
1423
- resolved_task_box = "todo"
1424
1251
  return {
1425
1252
  "task_id": task_id_text,
1426
- "task_box": resolved_task_box,
1427
1253
  "app_key": resolved_app_key,
1428
1254
  "record_id": resolved_record_id,
1429
1255
  "record_id_text": stringify_backend_id(resolved_record_id),
@@ -1481,7 +1307,6 @@ class TaskContextTools(ToolBase):
1481
1307
  resolved_record_id = int(locator["record_id"])
1482
1308
  record_id_text = str(locator["record_id_text"] or "")
1483
1309
  resolved_workflow_node_id = int(locator["workflow_node_id"])
1484
- resolved_task_box = str(locator.get("task_box") or "todo")
1485
1310
  self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
1486
1311
  task_context = self._build_task_context(
1487
1312
  profile=profile,
@@ -1489,7 +1314,6 @@ class TaskContextTools(ToolBase):
1489
1314
  app_key=resolved_app_key,
1490
1315
  record_id=resolved_record_id,
1491
1316
  workflow_node_id=resolved_workflow_node_id,
1492
- task_box=resolved_task_box,
1493
1317
  include_candidates=False,
1494
1318
  include_associated_reports=True,
1495
1319
  current_uid=session_profile.uid,
@@ -1563,7 +1387,7 @@ class TaskContextTools(ToolBase):
1563
1387
 
1564
1388
  chart_key = str(report_item.get("chart_key") or "")
1565
1389
  source_type = report_item.get("source_type")
1566
- if source_type in {"qingbi", "bi_qingflow", "bi_dataset"}:
1390
+ if source_type == "qingbi":
1567
1391
  qingbi_context = BackendRequestContext(
1568
1392
  base_url=_qingbi_base_url(context.base_url),
1569
1393
  token=context.token,
@@ -1572,44 +1396,20 @@ class TaskContextTools(ToolBase):
1572
1396
  qf_version=context.qf_version,
1573
1397
  qf_version_source=context.qf_version_source,
1574
1398
  )
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
- )
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
+ )
1613
1413
  request_route = {
1614
1414
  "base_url": qingbi_context.base_url,
1615
1415
  "qf_version": qingbi_context.qf_version,
@@ -1709,7 +1509,6 @@ class TaskContextTools(ToolBase):
1709
1509
  resolved_record_id = int(locator["record_id"])
1710
1510
  record_id_text = str(locator["record_id_text"] or "")
1711
1511
  resolved_workflow_node_id = int(locator["workflow_node_id"])
1712
- resolved_task_box = str(locator.get("task_box") or "todo")
1713
1512
  self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
1714
1513
  task_context = self._build_task_context(
1715
1514
  profile=profile,
@@ -1717,16 +1516,12 @@ class TaskContextTools(ToolBase):
1717
1516
  app_key=resolved_app_key,
1718
1517
  record_id=resolved_record_id,
1719
1518
  workflow_node_id=resolved_workflow_node_id,
1720
- task_box=resolved_task_box,
1721
1519
  include_candidates=False,
1722
1520
  include_associated_reports=False,
1723
1521
  current_uid=session_profile.uid,
1724
1522
  )
1725
1523
  visibility = task_context.get("visibility") or {}
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"):
1524
+ if not visibility.get("audit_record_visible"):
1730
1525
  raise_tool_error(
1731
1526
  QingflowApiError.config_error(
1732
1527
  f"workflow logs are not visible for app_key='{resolved_app_key}' record_id={record_id_text} workflow_node_id={resolved_workflow_node_id}"
@@ -1740,7 +1535,7 @@ class TaskContextTools(ToolBase):
1740
1535
  "key": resolved_app_key,
1741
1536
  "rowRecordId": resolved_record_id,
1742
1537
  "nodeId": resolved_workflow_node_id,
1743
- "role": _task_box_record_role(resolved_task_box),
1538
+ "role": 3,
1744
1539
  "pageNum": 1,
1745
1540
  "pageSize": 200,
1746
1541
  },
@@ -1751,7 +1546,7 @@ class TaskContextTools(ToolBase):
1751
1546
  "ws_id": session_profile.selected_ws_id,
1752
1547
  "ok": True,
1753
1548
  "request_route": self._request_route_payload(context),
1754
- "warnings": task_context.get("warnings") or [],
1549
+ "warnings": [],
1755
1550
  "output_profile": "normal",
1756
1551
  "data": {
1757
1552
  "selection": {
@@ -1760,9 +1555,7 @@ class TaskContextTools(ToolBase):
1760
1555
  "workflow_node_id": resolved_workflow_node_id,
1761
1556
  },
1762
1557
  "visibility": {
1763
- "audit_record_visible": (
1764
- visibility.get("audit_record_visible") if audit_visibility_explicit else None
1765
- ),
1558
+ "audit_record_visible": visibility.get("audit_record_visible"),
1766
1559
  "qrobot_record_visible": visibility.get("qrobot_record_visible"),
1767
1560
  },
1768
1561
  "items": items,
@@ -1787,94 +1580,43 @@ class TaskContextTools(ToolBase):
1787
1580
  include_candidates: bool,
1788
1581
  include_associated_reports: bool,
1789
1582
  current_uid: int | None = None,
1790
- task_box: str = "todo",
1791
1583
  ) -> dict[str, Any]:
1792
1584
  """执行内部辅助逻辑。"""
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(
1799
- "GET",
1800
- context,
1801
- f"/app/{app_key}/apply/{record_id}/auditInfo",
1802
- params={"type": list_type},
1803
- )
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
- )
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
+ )
1835
1598
  app_name = self._task_app_name(context=context, app_key=app_key, detail=detail, node_info=node_info)
1836
1599
  associated_report_visible = self._resolve_associated_report_visible(node_info, detail)
1837
- associated_reports = {"visible": associated_report_visible, "loaded": False, "count": 0, "items": [], "warnings": []}
1600
+ associated_reports = {"visible": associated_report_visible, "loaded": False, "count": 0, "items": []}
1838
1601
  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
- }
1602
+ asos_chart_list = self.backend.request(
1603
+ "GET",
1604
+ context,
1605
+ f"/app/{app_key}/asosChart",
1606
+ params={"role": 3, "auditNodeId": workflow_node_id, "beingDraft": False},
1607
+ )
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
+ }
1876
1619
  rollback_items: list[dict[str, Any]] = []
1877
- rollback_warnings: list[JSONObject] = []
1878
1620
  transfer_items: list[dict[str, Any]] = []
1879
1621
  transfer_warnings: list[JSONObject] = []
1880
1622
  transfer_pagination: JSONObject = {
@@ -1886,26 +1628,13 @@ class TaskContextTools(ToolBase):
1886
1628
  "truncated": False,
1887
1629
  }
1888
1630
  if include_candidates:
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
- )
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)
1909
1638
  transfer_items, transfer_warnings, transfer_pagination = self._transfer_candidate_items(
1910
1639
  context,
1911
1640
  app_key=app_key,
@@ -1928,7 +1657,6 @@ class TaskContextTools(ToolBase):
1928
1657
  context,
1929
1658
  app_key=app_key,
1930
1659
  workflow_node_id=workflow_node_id,
1931
- node_info=node_info,
1932
1660
  )
1933
1661
  capabilities = self._build_capabilities(
1934
1662
  node_info,
@@ -1975,9 +1703,7 @@ class TaskContextTools(ToolBase):
1975
1703
  "transfer_members": transfer_items,
1976
1704
  "loaded": include_candidates,
1977
1705
  "transfer_pagination": transfer_pagination,
1978
- "rollback_warnings": rollback_warnings,
1979
- "transfer_warnings": transfer_warnings,
1980
- "warnings": [*rollback_warnings, *transfer_warnings],
1706
+ "warnings": transfer_warnings,
1981
1707
  },
1982
1708
  "workflow_log_summary": {
1983
1709
  "visible": visibility["audit_record_visible"],
@@ -1986,7 +1712,6 @@ class TaskContextTools(ToolBase):
1986
1712
  "qrobot_log_visible": visibility["qrobot_record_visible"],
1987
1713
  },
1988
1714
  "update_schema": update_schema,
1989
- "warnings": context_warnings,
1990
1715
  }
1991
1716
 
1992
1717
  def _compact_task_get_context(self, data: dict[str, Any]) -> dict[str, Any]:
@@ -2056,14 +1781,12 @@ class TaskContextTools(ToolBase):
2056
1781
  "loaded": bool(associated_reports.get("loaded")),
2057
1782
  "count": len(associated_items),
2058
1783
  "items": associated_items,
2059
- "warnings": associated_reports.get("warnings") or [],
2060
1784
  },
2061
1785
  "rollback_candidates": {
2062
1786
  "available": "rollback" in available_actions,
2063
1787
  "loaded": bool(candidates.get("loaded")),
2064
1788
  "count": len(rollback_items),
2065
1789
  "items": rollback_items,
2066
- "warnings": candidates.get("rollback_warnings") or [],
2067
1790
  },
2068
1791
  "transfer_candidates": {
2069
1792
  "available": "transfer" in available_actions,
@@ -2071,7 +1794,7 @@ class TaskContextTools(ToolBase):
2071
1794
  "count": len(transfer_items),
2072
1795
  "items": transfer_items,
2073
1796
  "pagination": transfer_pagination,
2074
- "warnings": candidates.get("transfer_warnings") or [],
1797
+ "warnings": candidates.get("warnings") or [],
2075
1798
  },
2076
1799
  },
2077
1800
  }
@@ -2106,8 +1829,6 @@ class TaskContextTools(ToolBase):
2106
1829
  metadata["blockers"] = blockers
2107
1830
  if warnings:
2108
1831
  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")
2111
1832
  return metadata
2112
1833
 
2113
1834
  def _compact_initiator(self, payload: Any) -> dict[str, Any] | None:
@@ -2156,9 +1877,7 @@ class TaskContextTools(ToolBase):
2156
1877
  ) -> str | None:
2157
1878
  try:
2158
1879
  base_info = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
2159
- except QingflowApiError as exc:
2160
- if not _is_task_optional_read_error(exc):
2161
- raise
1880
+ except QingflowApiError:
2162
1881
  return None
2163
1882
  if not isinstance(base_info, dict):
2164
1883
  return None
@@ -2176,9 +1895,7 @@ class TaskContextTools(ToolBase):
2176
1895
  ) -> str | None:
2177
1896
  try:
2178
1897
  visible_apps = self.backend.request("GET", context, "/tag/apps")
2179
- except QingflowApiError as exc:
2180
- if not _is_task_optional_read_error(exc):
2181
- raise
1898
+ except QingflowApiError:
2182
1899
  return None
2183
1900
  return self._find_task_app_name_in_visible_apps(visible_apps, app_key=app_key)
2184
1901
 
@@ -2378,54 +2095,6 @@ class TaskContextTools(ToolBase):
2378
2095
  )
2379
2096
  )
2380
2097
 
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
-
2429
2098
  def _build_capabilities(
2430
2099
  self,
2431
2100
  node_info: dict[str, Any],
@@ -2490,18 +2159,35 @@ class TaskContextTools(ToolBase):
2490
2159
  try:
2491
2160
  app_schema = self._record_tools._get_form_schema(profile, context, app_key, force_refresh=False)
2492
2161
  except QingflowApiError as error:
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
- )
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
+ }
2505
2191
 
2506
2192
  question_relations = _collect_question_relations(app_schema)
2507
2193
  linked_field_ids = _collect_linked_required_field_ids(question_relations)
@@ -2574,7 +2260,6 @@ class TaskContextTools(ToolBase):
2574
2260
  "record_id": record_id_text,
2575
2261
  "workflow_node_id": workflow_node_id,
2576
2262
  },
2577
- "editable_question_ids_source": source,
2578
2263
  }
2579
2264
  return {
2580
2265
  "public_schema": public_schema,
@@ -2584,96 +2269,6 @@ class TaskContextTools(ToolBase):
2584
2269
  "editable_question_ids_source": source,
2585
2270
  }
2586
2271
 
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
-
2677
2272
  def _augment_task_editable_field_index(
2678
2273
  self,
2679
2274
  *,
@@ -2733,15 +2328,12 @@ class TaskContextTools(ToolBase):
2733
2328
  if question_ids:
2734
2329
  return question_ids, warnings, "workflow_editable_que_ids"
2735
2330
  except QingflowApiError as error:
2736
- if not _is_task_optional_read_error(error):
2331
+ if error.backend_code not in {40002, 40027, 404} and error.http_status != 404:
2737
2332
  raise
2738
2333
  warnings.append(
2739
2334
  {
2740
2335
  "code": "TASK_EDITABLE_IDS_FALLBACK",
2741
2336
  "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,
2745
2337
  }
2746
2338
  )
2747
2339
  fallback_ids = self._editable_ids_from_que_auth_setting(node_info.get("queAuthSetting"))
@@ -2753,7 +2345,6 @@ class TaskContextTools(ToolBase):
2753
2345
  *,
2754
2346
  app_key: str,
2755
2347
  workflow_node_id: int,
2756
- node_info: dict[str, Any],
2757
2348
  ) -> tuple[bool, list[JSONObject], str]:
2758
2349
  """执行内部辅助逻辑。"""
2759
2350
  try:
@@ -2763,28 +2354,15 @@ class TaskContextTools(ToolBase):
2763
2354
  f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
2764
2355
  )
2765
2356
  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"
2770
2357
  warning: JSONObject = {
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
- ),
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.",
2777
2360
  }
2778
2361
  if error.backend_code is not None:
2779
2362
  warning["backend_code"] = error.backend_code
2780
2363
  if error.http_status is not None:
2781
2364
  warning["http_status"] = error.http_status
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
2365
+ return False, [warning], "backend_editable_que_ids_unavailable"
2788
2366
  return bool(self._extract_question_ids(payload)), [], "workflow_editable_que_ids"
2789
2367
 
2790
2368
  def _extract_question_ids(self, payload: Any) -> set[int]:
@@ -3077,33 +2655,12 @@ class TaskContextTools(ToolBase):
3077
2655
  warnings: list[JSONObject] = []
3078
2656
 
3079
2657
  while page_num <= max_pages:
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
- }
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
+ )
3107
2664
  fetched_pages += 1
3108
2665
  raw_items = _approval_page_items(result)
3109
2666
  fetched_raw_count += len(raw_items)
@@ -3268,9 +2825,7 @@ class TaskContextTools(ToolBase):
3268
2825
  context,
3269
2826
  f"/chart/{chart_key}/auth",
3270
2827
  )
3271
- except QingflowApiError as error:
3272
- if not _is_task_optional_read_error(error):
3273
- raise
2828
+ except QingflowApiError:
3274
2829
  return False
3275
2830
  if not isinstance(auth, dict):
3276
2831
  return False
@@ -3447,62 +3002,3 @@ class TaskContextTools(ToolBase):
3447
3002
  "qf_version": context.qf_version,
3448
3003
  "qf_version_source": context.qf_version_source or ("context" if context.qf_version else "unknown"),
3449
3004
  }
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)