@qingflow-tech/qingflow-app-user-mcp 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +9 -3
  2. package/docs/local-agent-install.md +54 -3
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/lib/runtime.mjs +304 -13
  6. package/npm/scripts/postinstall.mjs +1 -5
  7. package/package.json +3 -2
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +255 -0
  10. package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
  11. package/skills/qingflow-app-builder/references/create-app.md +149 -0
  12. package/skills/qingflow-app-builder/references/environments.md +63 -0
  13. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  14. package/skills/qingflow-app-builder/references/gotchas.md +107 -0
  15. package/skills/qingflow-app-builder/references/match-rules.md +114 -0
  16. package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
  17. package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
  18. package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
  19. package/skills/qingflow-app-builder/references/update-flow.md +158 -0
  20. package/skills/qingflow-app-builder/references/update-layout.md +68 -0
  21. package/skills/qingflow-app-builder/references/update-schema.md +72 -0
  22. package/skills/qingflow-app-builder/references/update-views.md +284 -0
  23. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  24. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  25. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  26. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  27. package/skills/qingflow-app-user/SKILL.md +12 -11
  28. package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
  29. package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
  30. package/skills/qingflow-app-user/references/record-patterns.md +5 -5
  31. package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
  32. package/skills/qingflow-mcp-setup/SKILL.md +113 -0
  33. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  34. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  35. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  36. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  37. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  38. package/skills/qingflow-record-analysis/SKILL.md +6 -7
  39. package/skills/qingflow-record-analysis/manifest.yaml +10 -0
  40. package/skills/qingflow-record-delete/SKILL.md +5 -3
  41. package/skills/qingflow-record-import/SKILL.md +6 -2
  42. package/skills/qingflow-record-insert/SKILL.md +48 -4
  43. package/skills/qingflow-record-insert/manifest.yaml +6 -0
  44. package/skills/qingflow-record-update/SKILL.md +36 -24
  45. package/skills/qingflow-task-ops/SKILL.md +25 -25
  46. package/skills/qingflow-task-ops/references/environments.md +0 -1
  47. package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
  48. package/src/qingflow_mcp/__main__.py +6 -2
  49. package/src/qingflow_mcp/builder_facade/models.py +11 -0
  50. package/src/qingflow_mcp/builder_facade/service.py +1488 -288
  51. package/src/qingflow_mcp/cli/commands/builder.py +2 -2
  52. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  53. package/src/qingflow_mcp/cli/commands/imports.py +1 -1
  54. package/src/qingflow_mcp/cli/commands/record.py +91 -19
  55. package/src/qingflow_mcp/cli/context.py +0 -3
  56. package/src/qingflow_mcp/cli/formatters.py +206 -7
  57. package/src/qingflow_mcp/cli/main.py +47 -3
  58. package/src/qingflow_mcp/errors.py +43 -2
  59. package/src/qingflow_mcp/public_surface.py +21 -15
  60. package/src/qingflow_mcp/response_trim.py +74 -13
  61. package/src/qingflow_mcp/server.py +11 -9
  62. package/src/qingflow_mcp/server_app_builder.py +3 -2
  63. package/src/qingflow_mcp/server_app_user.py +19 -13
  64. package/src/qingflow_mcp/session_store.py +11 -7
  65. package/src/qingflow_mcp/solution/executor.py +112 -15
  66. package/src/qingflow_mcp/tools/ai_builder_tools.py +36 -11
  67. package/src/qingflow_mcp/tools/app_tools.py +184 -43
  68. package/src/qingflow_mcp/tools/approval_tools.py +196 -34
  69. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  70. package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
  71. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  72. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  73. package/src/qingflow_mcp/tools/export_tools.py +244 -34
  74. package/src/qingflow_mcp/tools/file_tools.py +7 -3
  75. package/src/qingflow_mcp/tools/import_tools.py +336 -49
  76. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  77. package/src/qingflow_mcp/tools/package_tools.py +118 -6
  78. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  79. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  80. package/src/qingflow_mcp/tools/record_tools.py +1067 -349
  81. package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
  82. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  83. package/src/qingflow_mcp/tools/solution_tools.py +57 -15
  84. package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
  85. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  86. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  87. package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
  88. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
@@ -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,
@@ -271,6 +271,7 @@ class TaskContextTools(ToolBase):
271
271
  resolved_app_key = str(locator["app_key"])
272
272
  resolved_record_id = int(locator["record_id"])
273
273
  resolved_workflow_node_id = int(locator["workflow_node_id"])
274
+ resolved_task_box = str(locator.get("task_box") or "todo")
274
275
  self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
275
276
  data = self._build_task_context(
276
277
  profile=profile,
@@ -278,10 +279,12 @@ class TaskContextTools(ToolBase):
278
279
  app_key=resolved_app_key,
279
280
  record_id=resolved_record_id,
280
281
  workflow_node_id=resolved_workflow_node_id,
282
+ task_box=resolved_task_box,
281
283
  include_candidates=include_candidates,
282
284
  include_associated_reports=include_associated_reports,
283
285
  current_uid=session_profile.uid,
284
286
  )
287
+ context_warnings = data.get("warnings") if isinstance(data.get("warnings"), list) else []
285
288
  data = self._compact_task_get_context(data)
286
289
  task_payload = data.get("task")
287
290
  if isinstance(task_payload, dict) and task_id_text is not None:
@@ -291,7 +294,7 @@ class TaskContextTools(ToolBase):
291
294
  "ws_id": session_profile.selected_ws_id,
292
295
  "ok": True,
293
296
  "request_route": self._request_route_payload(context),
294
- "warnings": [],
297
+ "warnings": context_warnings,
295
298
  "output_profile": "normal",
296
299
  "data": data,
297
300
  }
@@ -368,8 +371,63 @@ class TaskContextTools(ToolBase):
368
371
  resolved_record_id = int(locator["record_id"])
369
372
  resolved_record_id_text = str(locator["record_id_text"] or "")
370
373
  resolved_workflow_node_id = int(locator["workflow_node_id"])
374
+ resolved_task_box = str(locator.get("task_box") or "todo")
371
375
  record_id_text = resolved_record_id_text
372
376
  self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
377
+ if normalized_action == "urge":
378
+ raw = self._execute_task_action(
379
+ profile=profile,
380
+ app_key=resolved_app_key,
381
+ record_id=resolved_record_id,
382
+ workflow_node_id=resolved_workflow_node_id,
383
+ normalized_action=normalized_action,
384
+ payload=body,
385
+ prepared_fields=None,
386
+ )
387
+ verification, verification_warnings = self._verify_task_action_runtime(
388
+ profile=profile,
389
+ context=context,
390
+ app_key=resolved_app_key,
391
+ record_id=resolved_record_id,
392
+ workflow_node_id=resolved_workflow_node_id,
393
+ action=normalized_action,
394
+ before_apply_status=None,
395
+ runtime_baseline=None,
396
+ )
397
+ result = {
398
+ "profile": raw.get("profile", profile),
399
+ "ws_id": raw.get("ws_id", session_profile.selected_ws_id),
400
+ "ok": bool(raw.get("ok", True)),
401
+ "status": "success",
402
+ "error_code": None,
403
+ "action_executed": True,
404
+ "safe_to_retry": False,
405
+ "request_route": raw.get("request_route") or self._request_route_payload(context),
406
+ "warnings": verification_warnings,
407
+ "verification": verification,
408
+ "output_profile": "normal",
409
+ "data": {
410
+ "action": normalized_action,
411
+ "resource": {
412
+ "app_key": resolved_app_key,
413
+ "record_id": record_id_text,
414
+ "workflow_node_id": resolved_workflow_node_id,
415
+ },
416
+ "selection": {"action": normalized_action},
417
+ "result": raw.get("result"),
418
+ "human_review": True,
419
+ "field_update_applied": False,
420
+ },
421
+ }
422
+ if task_id_text is not None:
423
+ result["data"]["resource"]["task_id"] = task_id_text
424
+ return result
425
+ if resolved_task_box != "todo":
426
+ raise_tool_error(
427
+ QingflowApiError.config_error(
428
+ f"task_id={task_id_text or ''} resolved to task_box='{resolved_task_box}', but task_action_execute can only execute current todo tasks"
429
+ )
430
+ )
373
431
  try:
374
432
  task_context = self._build_task_context(
375
433
  profile=profile,
@@ -377,12 +435,13 @@ class TaskContextTools(ToolBase):
377
435
  app_key=resolved_app_key,
378
436
  record_id=resolved_record_id,
379
437
  workflow_node_id=resolved_workflow_node_id,
438
+ task_box=resolved_task_box,
380
439
  include_candidates=False,
381
440
  include_associated_reports=False,
382
441
  current_uid=session_profile.uid,
383
442
  )
384
443
  except QingflowApiError as error:
385
- if error.backend_code == 46001:
444
+ if backend_code_int(error) == 46001:
386
445
  return self._task_action_visibility_unverified_response(
387
446
  profile=profile,
388
447
  session_profile=session_profile,
@@ -470,7 +529,7 @@ class TaskContextTools(ToolBase):
470
529
  prepared_fields=prepared_fields,
471
530
  )
472
531
  except QingflowApiError as error:
473
- if error.backend_code == 46001:
532
+ if backend_code_int(error) == 46001:
474
533
  return self._task_action_visibility_unverified_response(
475
534
  profile=profile,
476
535
  session_profile=session_profile,
@@ -518,6 +577,8 @@ class TaskContextTools(ToolBase):
518
577
  "ok": bool(raw.get("ok", True)) and status != "failed",
519
578
  "status": status,
520
579
  "error_code": error_code,
580
+ "action_executed": True,
581
+ "safe_to_retry": False,
521
582
  "request_route": raw.get("request_route") or self._request_route_payload(context),
522
583
  "warnings": warnings,
523
584
  "verification": verification,
@@ -680,6 +741,8 @@ class TaskContextTools(ToolBase):
680
741
  "http_status": error.http_status,
681
742
  "backend_code": error.backend_code,
682
743
  "category": error.category,
744
+ "request_id": error.request_id,
745
+ "message": error.message,
683
746
  }
684
747
 
685
748
  log_items: list[dict[str, Any]] = []
@@ -707,6 +770,8 @@ class TaskContextTools(ToolBase):
707
770
  "http_status": error.http_status,
708
771
  "backend_code": error.backend_code,
709
772
  "category": error.category,
773
+ "request_id": error.request_id,
774
+ "message": error.message,
710
775
  }
711
776
 
712
777
  todo_items = self._safe_task_list_items(profile=profile, task_box="todo", app_key=app_key)
@@ -756,7 +821,7 @@ class TaskContextTools(ToolBase):
756
821
  runtime_consumed_after_action = bool(
757
822
  runtime_verified
758
823
  and isinstance(record_state_error, dict)
759
- and record_state_error.get("backend_code") == 46001
824
+ and backend_code_value_int(record_state_error.get("backend_code")) == 46001
760
825
  )
761
826
  if runtime_consumed_after_action:
762
827
  verification["record_state_scope"] = "current_node_runtime"
@@ -771,6 +836,28 @@ class TaskContextTools(ToolBase):
771
836
  ),
772
837
  }
773
838
  )
839
+ permission_blocked_sources: list[str] = []
840
+ if (
841
+ isinstance(verification.get("record_state_error"), dict)
842
+ and _is_permission_context_error_payload(verification["record_state_error"])
843
+ ):
844
+ permission_blocked_sources.append("record_state")
845
+ if (
846
+ isinstance(verification.get("workflow_log_error"), dict)
847
+ and _is_permission_context_error_payload(verification["workflow_log_error"])
848
+ ):
849
+ permission_blocked_sources.append("workflow_log")
850
+ if permission_blocked_sources:
851
+ warnings.append(
852
+ {
853
+ "code": "TASK_ACTION_VERIFICATION_PERMISSION_UNAVAILABLE",
854
+ "message": (
855
+ "task action executed, but some post-action verification reads are unavailable "
856
+ "in this permission context; do not treat the verification read failure as action denial."
857
+ ),
858
+ "sources": permission_blocked_sources,
859
+ }
860
+ )
774
861
  verification["runtime_continuation_verified"] = runtime_verified
775
862
  if not runtime_verified:
776
863
  warnings.append(
@@ -931,7 +1018,9 @@ class TaskContextTools(ToolBase):
931
1018
  baseline["workflow_log_visible"] = True
932
1019
  baseline["workflow_log_count"] = len(log_items)
933
1020
  baseline["workflow_log_digest"] = self._workflow_log_digest(log_items)
934
- except QingflowApiError:
1021
+ except QingflowApiError as exc:
1022
+ if is_auth_like_error(exc):
1023
+ raise
935
1024
  pass
936
1025
  todo_items = self._safe_task_list_items(profile=profile, task_box="todo", app_key=app_key)
937
1026
  baseline["downstream_todo_nodes"] = sorted(
@@ -1058,7 +1147,9 @@ class TaskContextTools(ToolBase):
1058
1147
  page=1,
1059
1148
  page_size=50,
1060
1149
  )
1061
- except QingflowApiError:
1150
+ except QingflowApiError as exc:
1151
+ if not _is_task_optional_read_error(exc):
1152
+ raise
1062
1153
  return []
1063
1154
  items = response.get("items") if isinstance(response, dict) else None
1064
1155
  if not isinstance(items, list):
@@ -1152,21 +1243,35 @@ class TaskContextTools(ToolBase):
1152
1243
  task_id_text = normalize_positive_id_text(task_id, field_name="task_id")
1153
1244
  searched_task_boxes = ("todo", "initiated", "cc", "done")
1154
1245
  incomplete_task_boxes: list[str] = []
1246
+ inaccessible_task_boxes: list[dict[str, Any]] = []
1155
1247
  page_size = 100
1156
1248
  for task_box in searched_task_boxes:
1157
1249
  page = 1
1158
1250
  page_amount: int | None = None
1159
1251
  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
- )
1252
+ try:
1253
+ response = self._list_normalized_task_items(
1254
+ profile=profile,
1255
+ task_box=task_box,
1256
+ flow_status="all",
1257
+ app_key=None,
1258
+ workflow_node_id=None,
1259
+ query=None,
1260
+ page=page,
1261
+ page_size=page_size,
1262
+ )
1263
+ except QingflowApiError as exc:
1264
+ if not _is_optional_task_box_locator_error(exc):
1265
+ raise
1266
+ inaccessible_task_boxes.append(
1267
+ {
1268
+ "task_box": task_box,
1269
+ "backend_code": exc.backend_code,
1270
+ "http_status": exc.http_status,
1271
+ "request_id": exc.request_id,
1272
+ }
1273
+ )
1274
+ break
1170
1275
  items = response.get("items") if isinstance(response.get("items"), list) else []
1171
1276
  for item in items:
1172
1277
  if not isinstance(item, dict) or not ids_equal(item.get("task_id"), task_id_text):
@@ -1200,6 +1305,18 @@ class TaskContextTools(ToolBase):
1200
1305
  f"task_id={task_id_text} resolved to an incomplete task locator in task_box={searched}; please refresh the task list and retry"
1201
1306
  )
1202
1307
  )
1308
+ if inaccessible_task_boxes:
1309
+ searched = ", ".join(str(item.get("task_box")) for item in inaccessible_task_boxes)
1310
+ raise_tool_error(
1311
+ QingflowApiError.config_error(
1312
+ 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}",
1313
+ details={
1314
+ "task_id": task_id_text,
1315
+ "searched_task_boxes": list(searched_task_boxes),
1316
+ "inaccessible_task_boxes": inaccessible_task_boxes,
1317
+ },
1318
+ )
1319
+ )
1203
1320
  raise_tool_error(
1204
1321
  QingflowApiError.config_error(
1205
1322
  f"task_id={task_id_text} was not found in the current visible task boxes (todo, initiated, cc, done)"
@@ -1224,6 +1341,7 @@ class TaskContextTools(ToolBase):
1224
1341
  resolved_app_key = str(locator["app_key"])
1225
1342
  resolved_record_id = normalize_positive_id_int(locator["record_id"], field_name="record_id")
1226
1343
  resolved_workflow_node_id = int(locator["workflow_node_id"])
1344
+ resolved_task_box = str(locator.get("task_box") or "todo")
1227
1345
  explicit_app_key = (app_key or "").strip()
1228
1346
  if explicit_app_key and explicit_app_key != resolved_app_key:
1229
1347
  raise_tool_error(
@@ -1248,8 +1366,10 @@ class TaskContextTools(ToolBase):
1248
1366
  else:
1249
1367
  resolved_record_id = normalize_positive_id_int(record_id, field_name="record_id")
1250
1368
  resolved_workflow_node_id = int(workflow_node_id)
1369
+ resolved_task_box = "todo"
1251
1370
  return {
1252
1371
  "task_id": task_id_text,
1372
+ "task_box": resolved_task_box,
1253
1373
  "app_key": resolved_app_key,
1254
1374
  "record_id": resolved_record_id,
1255
1375
  "record_id_text": stringify_backend_id(resolved_record_id),
@@ -1307,6 +1427,7 @@ class TaskContextTools(ToolBase):
1307
1427
  resolved_record_id = int(locator["record_id"])
1308
1428
  record_id_text = str(locator["record_id_text"] or "")
1309
1429
  resolved_workflow_node_id = int(locator["workflow_node_id"])
1430
+ resolved_task_box = str(locator.get("task_box") or "todo")
1310
1431
  self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
1311
1432
  task_context = self._build_task_context(
1312
1433
  profile=profile,
@@ -1314,6 +1435,7 @@ class TaskContextTools(ToolBase):
1314
1435
  app_key=resolved_app_key,
1315
1436
  record_id=resolved_record_id,
1316
1437
  workflow_node_id=resolved_workflow_node_id,
1438
+ task_box=resolved_task_box,
1317
1439
  include_candidates=False,
1318
1440
  include_associated_reports=True,
1319
1441
  current_uid=session_profile.uid,
@@ -1387,7 +1509,7 @@ class TaskContextTools(ToolBase):
1387
1509
 
1388
1510
  chart_key = str(report_item.get("chart_key") or "")
1389
1511
  source_type = report_item.get("source_type")
1390
- if source_type == "qingbi":
1512
+ if source_type in {"qingbi", "bi_qingflow", "bi_dataset"}:
1391
1513
  qingbi_context = BackendRequestContext(
1392
1514
  base_url=_qingbi_base_url(context.base_url),
1393
1515
  token=context.token,
@@ -1396,20 +1518,44 @@ class TaskContextTools(ToolBase):
1396
1518
  qf_version=context.qf_version,
1397
1519
  qf_version_source=context.qf_version_source,
1398
1520
  )
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
- )
1521
+ chart_payload = {
1522
+ "asosChartId": report_id,
1523
+ "keyQueValues": association_query.get("keyQueValues") or [],
1524
+ }
1525
+ chart_params = {
1526
+ "qfUUID": uuid4().hex,
1527
+ "pageNum": page,
1528
+ "pageSize": page_size,
1529
+ }
1530
+ try:
1531
+ chart_result = self.backend.request(
1532
+ "POST",
1533
+ qingbi_context,
1534
+ f"/qingbi/charts/data/qflow/{chart_key}/detail",
1535
+ params=chart_params,
1536
+ json_body=chart_payload,
1537
+ )
1538
+ except QingflowApiError as error:
1539
+ if not _should_retry_asos_data(error):
1540
+ raise
1541
+ try:
1542
+ chart_result = self.backend.request(
1543
+ "POST",
1544
+ qingbi_context,
1545
+ f"/qingbi/charts/data/qflow/{chart_key}/asos",
1546
+ params=chart_params,
1547
+ json_body=chart_payload,
1548
+ )
1549
+ except QingflowApiError as fallback_error:
1550
+ if not _should_retry_asos_data(fallback_error):
1551
+ raise
1552
+ chart_result = self.backend.request(
1553
+ "POST",
1554
+ qingbi_context,
1555
+ f"/qingbi/charts/data/{chart_key}",
1556
+ params=chart_params,
1557
+ json_body=chart_payload,
1558
+ )
1413
1559
  request_route = {
1414
1560
  "base_url": qingbi_context.base_url,
1415
1561
  "qf_version": qingbi_context.qf_version,
@@ -1509,6 +1655,7 @@ class TaskContextTools(ToolBase):
1509
1655
  resolved_record_id = int(locator["record_id"])
1510
1656
  record_id_text = str(locator["record_id_text"] or "")
1511
1657
  resolved_workflow_node_id = int(locator["workflow_node_id"])
1658
+ resolved_task_box = str(locator.get("task_box") or "todo")
1512
1659
  self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
1513
1660
  task_context = self._build_task_context(
1514
1661
  profile=profile,
@@ -1516,12 +1663,16 @@ class TaskContextTools(ToolBase):
1516
1663
  app_key=resolved_app_key,
1517
1664
  record_id=resolved_record_id,
1518
1665
  workflow_node_id=resolved_workflow_node_id,
1666
+ task_box=resolved_task_box,
1519
1667
  include_candidates=False,
1520
1668
  include_associated_reports=False,
1521
1669
  current_uid=session_profile.uid,
1522
1670
  )
1523
1671
  visibility = task_context.get("visibility") or {}
1524
- if not visibility.get("audit_record_visible"):
1672
+ node = task_context.get("node") if isinstance(task_context.get("node"), dict) else {}
1673
+ raw_node = node.get("raw") if isinstance(node.get("raw"), dict) else {}
1674
+ audit_visibility_explicit = "auditRecordVisible" in raw_node
1675
+ if audit_visibility_explicit and not visibility.get("audit_record_visible"):
1525
1676
  raise_tool_error(
1526
1677
  QingflowApiError.config_error(
1527
1678
  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 +1686,7 @@ class TaskContextTools(ToolBase):
1535
1686
  "key": resolved_app_key,
1536
1687
  "rowRecordId": resolved_record_id,
1537
1688
  "nodeId": resolved_workflow_node_id,
1538
- "role": 3,
1689
+ "role": _task_box_record_role(resolved_task_box),
1539
1690
  "pageNum": 1,
1540
1691
  "pageSize": 200,
1541
1692
  },
@@ -1546,7 +1697,7 @@ class TaskContextTools(ToolBase):
1546
1697
  "ws_id": session_profile.selected_ws_id,
1547
1698
  "ok": True,
1548
1699
  "request_route": self._request_route_payload(context),
1549
- "warnings": [],
1700
+ "warnings": task_context.get("warnings") or [],
1550
1701
  "output_profile": "normal",
1551
1702
  "data": {
1552
1703
  "selection": {
@@ -1555,7 +1706,9 @@ class TaskContextTools(ToolBase):
1555
1706
  "workflow_node_id": resolved_workflow_node_id,
1556
1707
  },
1557
1708
  "visibility": {
1558
- "audit_record_visible": visibility.get("audit_record_visible"),
1709
+ "audit_record_visible": (
1710
+ visibility.get("audit_record_visible") if audit_visibility_explicit else None
1711
+ ),
1559
1712
  "qrobot_record_visible": visibility.get("qrobot_record_visible"),
1560
1713
  },
1561
1714
  "items": items,
@@ -1580,43 +1733,94 @@ class TaskContextTools(ToolBase):
1580
1733
  include_candidates: bool,
1581
1734
  include_associated_reports: bool,
1582
1735
  current_uid: int | None = None,
1736
+ task_box: str = "todo",
1583
1737
  ) -> dict[str, Any]:
1584
1738
  """执行内部辅助逻辑。"""
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(
1739
+ context_warnings: list[JSONObject] = []
1740
+ detail: dict[str, Any] | None = None
1741
+ list_type = _task_box_record_list_type(task_box)
1742
+ role = _task_box_record_role(task_box)
1743
+ try:
1744
+ audit_infos = self.backend.request(
1603
1745
  "GET",
1604
1746
  context,
1605
- f"/app/{app_key}/asosChart",
1606
- params={"role": 3, "auditNodeId": workflow_node_id, "beingDraft": False},
1747
+ f"/app/{app_key}/apply/{record_id}/auditInfo",
1748
+ params={"type": list_type},
1607
1749
  )
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
- }
1750
+ node_info = self._select_task_node(audit_infos, workflow_node_id, app_key=app_key, record_id=record_id)
1751
+ except QingflowApiError as exc:
1752
+ if not _is_optional_task_audit_info_error(exc):
1753
+ raise
1754
+ detail = self.backend.request(
1755
+ "GET",
1756
+ context,
1757
+ f"/app/{app_key}/apply/{record_id}",
1758
+ params={"role": role, "listType": list_type, "auditNodeId": workflow_node_id},
1759
+ )
1760
+ node_info = self._fallback_task_node_from_detail(
1761
+ detail,
1762
+ workflow_node_id=workflow_node_id,
1763
+ source_error=exc,
1764
+ )
1765
+ context_warnings.append(
1766
+ {
1767
+ "code": "TASK_AUDIT_INFO_UNAVAILABLE",
1768
+ "message": "Task context used the current task detail because auditInfo was unavailable in this permission context.",
1769
+ "backend_code": exc.backend_code,
1770
+ "request_id": exc.request_id,
1771
+ "http_status": exc.http_status,
1772
+ }
1773
+ )
1774
+ if detail is None:
1775
+ detail = self.backend.request(
1776
+ "GET",
1777
+ context,
1778
+ f"/app/{app_key}/apply/{record_id}",
1779
+ params={"role": role, "listType": list_type, "auditNodeId": workflow_node_id},
1780
+ )
1781
+ app_name = self._task_app_name(context=context, app_key=app_key, detail=detail, node_info=node_info)
1782
+ associated_report_visible = self._resolve_associated_report_visible(node_info, detail)
1783
+ associated_reports = {"visible": associated_report_visible, "loaded": False, "count": 0, "items": [], "warnings": []}
1784
+ if include_associated_reports and associated_report_visible:
1785
+ try:
1786
+ asos_chart_list = self.backend.request(
1787
+ "GET",
1788
+ context,
1789
+ f"/app/{app_key}/asosChart",
1790
+ params={"role": role, "auditNodeId": workflow_node_id, "beingDraft": False},
1791
+ )
1792
+ associated_items = [
1793
+ self._normalize_associated_report(item)
1794
+ for item in (asos_chart_list.get("asosCharts") or [])
1795
+ if isinstance(item, dict)
1796
+ ]
1797
+ associated_reports = {
1798
+ "visible": True,
1799
+ "loaded": True,
1800
+ "count": len(associated_items),
1801
+ "items": associated_items,
1802
+ "warnings": [],
1803
+ }
1804
+ except QingflowApiError as exc:
1805
+ if not _is_task_optional_read_error(exc):
1806
+ raise
1807
+ associated_reports = {
1808
+ "visible": True,
1809
+ "loaded": False,
1810
+ "count": 0,
1811
+ "items": [],
1812
+ "warnings": [
1813
+ {
1814
+ "code": "TASK_ASSOCIATED_REPORTS_UNAVAILABLE",
1815
+ "message": "Associated reports are not readable in this permission context; task detail remains available.",
1816
+ "backend_code": exc.backend_code,
1817
+ "request_id": exc.request_id,
1818
+ "http_status": exc.http_status,
1819
+ }
1820
+ ],
1821
+ }
1619
1822
  rollback_items: list[dict[str, Any]] = []
1823
+ rollback_warnings: list[JSONObject] = []
1620
1824
  transfer_items: list[dict[str, Any]] = []
1621
1825
  transfer_warnings: list[JSONObject] = []
1622
1826
  transfer_pagination: JSONObject = {
@@ -1628,13 +1832,26 @@ class TaskContextTools(ToolBase):
1628
1832
  "truncated": False,
1629
1833
  }
1630
1834
  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)
1835
+ try:
1836
+ rollback_result = self.backend.request(
1837
+ "GET",
1838
+ context,
1839
+ f"/app/{app_key}/apply/{record_id}/revertNode",
1840
+ params={"auditNodeId": workflow_node_id},
1841
+ )
1842
+ rollback_items = self._rollback_candidate_items(rollback_result)
1843
+ except QingflowApiError as exc:
1844
+ if not _is_task_optional_read_error(exc):
1845
+ raise
1846
+ rollback_warnings.append(
1847
+ {
1848
+ "code": "TASK_ROLLBACK_CANDIDATES_UNAVAILABLE",
1849
+ "message": "Rollback candidates are not readable in this permission context; task detail remains available.",
1850
+ "backend_code": exc.backend_code,
1851
+ "request_id": exc.request_id,
1852
+ "http_status": exc.http_status,
1853
+ }
1854
+ )
1638
1855
  transfer_items, transfer_warnings, transfer_pagination = self._transfer_candidate_items(
1639
1856
  context,
1640
1857
  app_key=app_key,
@@ -1657,6 +1874,7 @@ class TaskContextTools(ToolBase):
1657
1874
  context,
1658
1875
  app_key=app_key,
1659
1876
  workflow_node_id=workflow_node_id,
1877
+ node_info=node_info,
1660
1878
  )
1661
1879
  capabilities = self._build_capabilities(
1662
1880
  node_info,
@@ -1703,7 +1921,9 @@ class TaskContextTools(ToolBase):
1703
1921
  "transfer_members": transfer_items,
1704
1922
  "loaded": include_candidates,
1705
1923
  "transfer_pagination": transfer_pagination,
1706
- "warnings": transfer_warnings,
1924
+ "rollback_warnings": rollback_warnings,
1925
+ "transfer_warnings": transfer_warnings,
1926
+ "warnings": [*rollback_warnings, *transfer_warnings],
1707
1927
  },
1708
1928
  "workflow_log_summary": {
1709
1929
  "visible": visibility["audit_record_visible"],
@@ -1712,6 +1932,7 @@ class TaskContextTools(ToolBase):
1712
1932
  "qrobot_log_visible": visibility["qrobot_record_visible"],
1713
1933
  },
1714
1934
  "update_schema": update_schema,
1935
+ "warnings": context_warnings,
1715
1936
  }
1716
1937
 
1717
1938
  def _compact_task_get_context(self, data: dict[str, Any]) -> dict[str, Any]:
@@ -1781,12 +2002,14 @@ class TaskContextTools(ToolBase):
1781
2002
  "loaded": bool(associated_reports.get("loaded")),
1782
2003
  "count": len(associated_items),
1783
2004
  "items": associated_items,
2005
+ "warnings": associated_reports.get("warnings") or [],
1784
2006
  },
1785
2007
  "rollback_candidates": {
1786
2008
  "available": "rollback" in available_actions,
1787
2009
  "loaded": bool(candidates.get("loaded")),
1788
2010
  "count": len(rollback_items),
1789
2011
  "items": rollback_items,
2012
+ "warnings": candidates.get("rollback_warnings") or [],
1790
2013
  },
1791
2014
  "transfer_candidates": {
1792
2015
  "available": "transfer" in available_actions,
@@ -1794,7 +2017,7 @@ class TaskContextTools(ToolBase):
1794
2017
  "count": len(transfer_items),
1795
2018
  "items": transfer_items,
1796
2019
  "pagination": transfer_pagination,
1797
- "warnings": candidates.get("warnings") or [],
2020
+ "warnings": candidates.get("transfer_warnings") or [],
1798
2021
  },
1799
2022
  },
1800
2023
  }
@@ -1829,6 +2052,8 @@ class TaskContextTools(ToolBase):
1829
2052
  metadata["blockers"] = blockers
1830
2053
  if warnings:
1831
2054
  metadata["warnings"] = warnings
2055
+ if update_schema.get("editable_question_ids_source"):
2056
+ metadata["editable_question_ids_source"] = update_schema.get("editable_question_ids_source")
1832
2057
  return metadata
1833
2058
 
1834
2059
  def _compact_initiator(self, payload: Any) -> dict[str, Any] | None:
@@ -1877,7 +2102,9 @@ class TaskContextTools(ToolBase):
1877
2102
  ) -> str | None:
1878
2103
  try:
1879
2104
  base_info = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
1880
- except QingflowApiError:
2105
+ except QingflowApiError as exc:
2106
+ if not _is_task_optional_read_error(exc):
2107
+ raise
1881
2108
  return None
1882
2109
  if not isinstance(base_info, dict):
1883
2110
  return None
@@ -1895,7 +2122,9 @@ class TaskContextTools(ToolBase):
1895
2122
  ) -> str | None:
1896
2123
  try:
1897
2124
  visible_apps = self.backend.request("GET", context, "/tag/apps")
1898
- except QingflowApiError:
2125
+ except QingflowApiError as exc:
2126
+ if not _is_task_optional_read_error(exc):
2127
+ raise
1899
2128
  return None
1900
2129
  return self._find_task_app_name_in_visible_apps(visible_apps, app_key=app_key)
1901
2130
 
@@ -2095,6 +2324,54 @@ class TaskContextTools(ToolBase):
2095
2324
  )
2096
2325
  )
2097
2326
 
2327
+ def _fallback_task_node_from_detail(
2328
+ self,
2329
+ detail: dict[str, Any],
2330
+ *,
2331
+ workflow_node_id: int,
2332
+ source_error: QingflowApiError,
2333
+ ) -> dict[str, Any]:
2334
+ """Build a conservative task node snapshot from the readable task detail."""
2335
+ node_info: dict[str, Any] = {
2336
+ "auditNodeId": workflow_node_id,
2337
+ "nodeId": workflow_node_id,
2338
+ "auditNodeName": (
2339
+ detail.get("auditNodeName")
2340
+ or detail.get("nodeName")
2341
+ or detail.get("workflowNodeName")
2342
+ or detail.get("currentNodeName")
2343
+ ),
2344
+ "_auditInfoUnavailable": True,
2345
+ "_auditInfoError": {
2346
+ "http_status": source_error.http_status,
2347
+ "backend_code": source_error.backend_code,
2348
+ "category": source_error.category,
2349
+ },
2350
+ }
2351
+ for key in (
2352
+ "canTransfer",
2353
+ "canRevert",
2354
+ "canUrge",
2355
+ "rejectBtnStatus",
2356
+ "canRevoke",
2357
+ "beingEndWorkflow",
2358
+ "beingCanApplyAgain",
2359
+ "beingSubmitCheck",
2360
+ "beingSubmitPreview",
2361
+ "feedbackRequiredOperationType",
2362
+ "queAuthSetting",
2363
+ "auditRecordVisible",
2364
+ "qrobotRecordBeingVisible",
2365
+ "beingWorkflowNodeFutureListVisible",
2366
+ "commentStatus",
2367
+ "asosChartVisible",
2368
+ ):
2369
+ if key in detail:
2370
+ node_info[key] = detail[key]
2371
+ if "viewAsosChartVisible" in detail and "asosChartVisible" not in node_info:
2372
+ node_info["asosChartVisible"] = detail.get("viewAsosChartVisible")
2373
+ return node_info
2374
+
2098
2375
  def _build_capabilities(
2099
2376
  self,
2100
2377
  node_info: dict[str, Any],
@@ -2159,35 +2436,18 @@ class TaskContextTools(ToolBase):
2159
2436
  try:
2160
2437
  app_schema = self._record_tools._get_form_schema(profile, context, app_key, force_refresh=False)
2161
2438
  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
- }
2439
+ if not _is_task_optional_read_error(error):
2440
+ raise
2441
+ return self._build_task_update_schema_from_runtime_answers(
2442
+ profile=profile,
2443
+ context=context,
2444
+ app_key=app_key,
2445
+ record_id=record_id,
2446
+ workflow_node_id=workflow_node_id,
2447
+ node_info=node_info,
2448
+ current_answers=current_answers,
2449
+ source_error=error,
2450
+ )
2191
2451
 
2192
2452
  question_relations = _collect_question_relations(app_schema)
2193
2453
  linked_field_ids = _collect_linked_required_field_ids(question_relations)
@@ -2260,6 +2520,7 @@ class TaskContextTools(ToolBase):
2260
2520
  "record_id": record_id_text,
2261
2521
  "workflow_node_id": workflow_node_id,
2262
2522
  },
2523
+ "editable_question_ids_source": source,
2263
2524
  }
2264
2525
  return {
2265
2526
  "public_schema": public_schema,
@@ -2269,6 +2530,96 @@ class TaskContextTools(ToolBase):
2269
2530
  "editable_question_ids_source": source,
2270
2531
  }
2271
2532
 
2533
+ def _build_task_update_schema_from_runtime_answers(
2534
+ self,
2535
+ *,
2536
+ profile: str,
2537
+ context: BackendRequestContext,
2538
+ app_key: str,
2539
+ record_id: int,
2540
+ workflow_node_id: int,
2541
+ node_info: dict[str, Any],
2542
+ current_answers: Any,
2543
+ source_error: QingflowApiError,
2544
+ ) -> dict[str, Any]:
2545
+ """Build node-scoped edit schema from the task detail when app schema is unavailable."""
2546
+ record_id_text = stringify_backend_id(record_id)
2547
+ editable_question_ids, schema_warnings, source = self._resolve_task_editable_question_ids(
2548
+ context,
2549
+ app_key=app_key,
2550
+ workflow_node_id=workflow_node_id,
2551
+ node_info=node_info,
2552
+ )
2553
+ schema_warnings.insert(
2554
+ 0,
2555
+ {
2556
+ "code": "TASK_UPDATE_SCHEMA_APP_SCHEMA_UNAVAILABLE",
2557
+ "message": "task update schema used current task answers because the app applicant schema was unavailable in this permission context.",
2558
+ "transport_error": {
2559
+ "http_status": source_error.http_status,
2560
+ "backend_code": source_error.backend_code,
2561
+ "category": source_error.category,
2562
+ },
2563
+ },
2564
+ )
2565
+ index = _build_answer_backed_field_index(
2566
+ current_answers,
2567
+ field_id_filter={str(que_id) for que_id in editable_question_ids if que_id > 0},
2568
+ )
2569
+ effective_editable_ids = set(editable_question_ids)
2570
+ writable_fields: list[JSONObject] = []
2571
+ for field in index.by_id.values():
2572
+ if field.que_type in LAYOUT_ONLY_QUE_TYPES or field.que_id not in effective_editable_ids:
2573
+ continue
2574
+ editable_field = _clone_form_field(field, readonly=False)
2575
+ write_hints = self._record_tools._schema_write_hints(editable_field)
2576
+ if not bool(write_hints.get("writable")):
2577
+ continue
2578
+ writable_field = self._record_tools._ready_schema_field_payload(
2579
+ profile,
2580
+ context,
2581
+ editable_field,
2582
+ ws_id=context.ws_id,
2583
+ required_override=False,
2584
+ linkage_payloads_by_field_id={},
2585
+ )
2586
+ writable_field.setdefault("field_id", editable_field.que_id)
2587
+ writable_fields.append(writable_field)
2588
+
2589
+ blockers: list[str] = []
2590
+ if not writable_fields:
2591
+ blockers.append("NO_TASK_EDITABLE_FIELDS")
2592
+ schema_warnings.append(
2593
+ {
2594
+ "code": "NO_TASK_EDITABLE_FIELDS",
2595
+ "message": "the current task node does not expose any writable fields in the current task detail.",
2596
+ }
2597
+ )
2598
+ public_schema: JSONObject = {
2599
+ "schema_scope": "task_update_ready",
2600
+ "writable_fields": writable_fields,
2601
+ "payload_template": {
2602
+ item["title"]: self._record_tools._ready_schema_template_value(item)
2603
+ for item in writable_fields
2604
+ if isinstance(item, dict) and item.get("title")
2605
+ },
2606
+ "blockers": blockers,
2607
+ "warnings": schema_warnings,
2608
+ "selection": {
2609
+ "app_key": app_key,
2610
+ "record_id": record_id_text,
2611
+ "workflow_node_id": workflow_node_id,
2612
+ },
2613
+ "editable_question_ids_source": f"{source}_runtime_answers",
2614
+ }
2615
+ return {
2616
+ "public_schema": public_schema,
2617
+ "index": index,
2618
+ "editable_question_ids": sorted(editable_question_ids),
2619
+ "effective_editable_question_ids": sorted(effective_editable_ids),
2620
+ "editable_question_ids_source": f"{source}_runtime_answers",
2621
+ }
2622
+
2272
2623
  def _augment_task_editable_field_index(
2273
2624
  self,
2274
2625
  *,
@@ -2328,12 +2679,15 @@ class TaskContextTools(ToolBase):
2328
2679
  if question_ids:
2329
2680
  return question_ids, warnings, "workflow_editable_que_ids"
2330
2681
  except QingflowApiError as error:
2331
- if error.backend_code not in {40002, 40027, 404} and error.http_status != 404:
2682
+ if not _is_task_optional_read_error(error):
2332
2683
  raise
2333
2684
  warnings.append(
2334
2685
  {
2335
2686
  "code": "TASK_EDITABLE_IDS_FALLBACK",
2336
2687
  "message": "editable question ids endpoint is unavailable in the current route; task update schema fell back to queAuthSetting and may be conservative.",
2688
+ "backend_code": error.backend_code,
2689
+ "http_status": error.http_status,
2690
+ "request_id": error.request_id,
2337
2691
  }
2338
2692
  )
2339
2693
  fallback_ids = self._editable_ids_from_que_auth_setting(node_info.get("queAuthSetting"))
@@ -2345,6 +2699,7 @@ class TaskContextTools(ToolBase):
2345
2699
  *,
2346
2700
  app_key: str,
2347
2701
  workflow_node_id: int,
2702
+ node_info: dict[str, Any],
2348
2703
  ) -> tuple[bool, list[JSONObject], str]:
2349
2704
  """执行内部辅助逻辑。"""
2350
2705
  try:
@@ -2354,15 +2709,28 @@ class TaskContextTools(ToolBase):
2354
2709
  f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
2355
2710
  )
2356
2711
  except QingflowApiError as error:
2712
+ if not _is_task_optional_read_error(error):
2713
+ raise
2714
+ fallback_ids = self._editable_ids_from_que_auth_setting(node_info.get("queAuthSetting"))
2715
+ fallback_source = "que_auth_setting" if fallback_ids else "backend_editable_que_ids_unavailable"
2357
2716
  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.",
2717
+ "code": "TASK_SAVE_ONLY_EDITABLE_IDS_FALLBACK" if fallback_ids else "TASK_SAVE_ONLY_SIGNAL_UNAVAILABLE",
2718
+ "message": (
2719
+ "save_only availability used current task queAuthSetting because backend editableQueIds was unavailable in this permission context."
2720
+ if fallback_ids
2721
+ else "save_only is hidden because backend editableQueIds is unavailable and current task detail does not expose editable fields."
2722
+ ),
2360
2723
  }
2361
2724
  if error.backend_code is not None:
2362
2725
  warning["backend_code"] = error.backend_code
2363
2726
  if error.http_status is not None:
2364
2727
  warning["http_status"] = error.http_status
2365
- return False, [warning], "backend_editable_que_ids_unavailable"
2728
+ if error.request_id is not None:
2729
+ warning["request_id"] = error.request_id
2730
+ if fallback_ids:
2731
+ warning["field_ids"] = sorted(fallback_ids)
2732
+ return True, [warning], fallback_source
2733
+ return False, [warning], fallback_source
2366
2734
  return bool(self._extract_question_ids(payload)), [], "workflow_editable_que_ids"
2367
2735
 
2368
2736
  def _extract_question_ids(self, payload: Any) -> set[int]:
@@ -2655,12 +3023,33 @@ class TaskContextTools(ToolBase):
2655
3023
  warnings: list[JSONObject] = []
2656
3024
 
2657
3025
  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
- )
3026
+ try:
3027
+ result = self.backend.request(
3028
+ "GET",
3029
+ context,
3030
+ f"/app/{app_key}/apply/{record_id}/transfer/member",
3031
+ params={"pageNum": page_num, "pageSize": page_size, "auditNodeId": workflow_node_id},
3032
+ )
3033
+ except QingflowApiError as exc:
3034
+ if not _is_task_optional_read_error(exc):
3035
+ raise
3036
+ warnings.append(
3037
+ {
3038
+ "code": "TASK_TRANSFER_CANDIDATES_UNAVAILABLE",
3039
+ "message": "Transfer candidates are not readable in this permission context; task detail remains available.",
3040
+ "backend_code": exc.backend_code,
3041
+ "request_id": exc.request_id,
3042
+ "http_status": exc.http_status,
3043
+ }
3044
+ )
3045
+ return items, warnings, {
3046
+ "loaded": False,
3047
+ "page_size": page_size,
3048
+ "fetched_pages": fetched_pages,
3049
+ "reported_total": reported_total,
3050
+ "page_amount": page_amount,
3051
+ "truncated": False,
3052
+ }
2664
3053
  fetched_pages += 1
2665
3054
  raw_items = _approval_page_items(result)
2666
3055
  fetched_raw_count += len(raw_items)
@@ -2825,7 +3214,9 @@ class TaskContextTools(ToolBase):
2825
3214
  context,
2826
3215
  f"/chart/{chart_key}/auth",
2827
3216
  )
2828
- except QingflowApiError:
3217
+ except QingflowApiError as error:
3218
+ if not _is_task_optional_read_error(error):
3219
+ raise
2829
3220
  return False
2830
3221
  if not isinstance(auth, dict):
2831
3222
  return False
@@ -3002,3 +3393,62 @@ class TaskContextTools(ToolBase):
3002
3393
  "qf_version": context.qf_version,
3003
3394
  "qf_version_source": context.qf_version_source or ("context" if context.qf_version else "unknown"),
3004
3395
  }
3396
+
3397
+
3398
+ def _is_optional_task_audit_info_error(error: QingflowApiError) -> bool:
3399
+ """Whether task auditInfo may be replaced by current task detail."""
3400
+ if is_auth_like_error(error):
3401
+ return False
3402
+ return _is_task_permission_context_error(error) or error.http_status == 404
3403
+
3404
+
3405
+ def _task_box_record_list_type(task_box: str | None) -> int:
3406
+ normalized = str(task_box or "todo").strip().lower()
3407
+ if normalized == "initiated":
3408
+ return 14
3409
+ if normalized == "done":
3410
+ return 2
3411
+ if normalized == "cc":
3412
+ return 12
3413
+ return 1
3414
+
3415
+
3416
+ def _task_box_record_role(task_box: str | None) -> int:
3417
+ normalized = str(task_box or "todo").strip().lower()
3418
+ if normalized == "initiated":
3419
+ return 2
3420
+ return 3
3421
+
3422
+
3423
+ def _is_optional_task_box_locator_error(error: QingflowApiError) -> bool:
3424
+ """Whether one task box may be skipped while resolving a task_id."""
3425
+ if is_auth_like_error(error):
3426
+ return False
3427
+ return _is_task_permission_context_error(error) or error.http_status == 404
3428
+
3429
+
3430
+ def _is_task_optional_read_error(error: QingflowApiError) -> bool:
3431
+ if is_auth_like_error(error):
3432
+ return False
3433
+ backend_code = _task_backend_code(error)
3434
+ return backend_code in {40002, 40027, 404} or error.http_status == 404
3435
+
3436
+
3437
+ def _is_task_permission_context_error(error: QingflowApiError) -> bool:
3438
+ if is_auth_like_error(error):
3439
+ return False
3440
+ return _task_backend_code(error) in {40002, 40027}
3441
+
3442
+
3443
+ def _is_permission_context_error_payload(payload: dict[str, Any]) -> bool:
3444
+ if str(payload.get("category") or "").strip().lower() == "auth":
3445
+ return False
3446
+ if message_looks_like_invalid_token(payload.get("message")):
3447
+ return False
3448
+ if backend_code_value_int(payload.get("http_status")) == 401:
3449
+ return False
3450
+ return backend_code_value_int(payload.get("backend_code")) in {40002, 40027}
3451
+
3452
+
3453
+ def _task_backend_code(error: QingflowApiError) -> int | None:
3454
+ return backend_code_int(error)