@josephyan/qingflow-cli 1.1.3 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +7 -3
  2. package/docs/local-agent-install.md +57 -6
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/bin/qingflow.mjs +1 -34
  6. package/npm/lib/runtime.mjs +21 -101
  7. package/npm/scripts/postinstall.mjs +1 -10
  8. package/package.json +3 -2
  9. package/pyproject.toml +1 -1
  10. package/skills/qingflow-cli/SKILL.md +58 -44
  11. package/skills/qingflow-cli/manifest.yaml +1 -1
  12. package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
  13. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
  14. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
  15. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
  16. package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
  17. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
  18. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
  19. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
  21. package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
  22. package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
  23. package/skills/qingflow-cli/reference/builder/README.md +41 -0
  24. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
  25. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
  26. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
  27. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
  28. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
  29. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
  30. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
  31. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
  32. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
  33. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
  34. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
  35. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
  36. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
  37. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
  38. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
  39. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
  40. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
  41. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
  42. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
  43. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
  44. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
  45. package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
  46. package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
  47. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
  48. package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
  49. package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
  50. package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
  51. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
  52. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
  53. package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
  54. package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
  55. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
  56. package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
  57. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
  58. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
  59. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
  60. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
  61. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
  62. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
  63. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
  64. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
  65. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
  66. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
  67. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
  68. package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
  69. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
  70. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
  71. package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
  72. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
  73. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
  74. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
  75. package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
  76. package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
  77. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
  78. package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
  79. package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
  80. package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
  81. package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
  82. package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
  83. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
  84. package/skills/qingflow-mcp-setup/SKILL.md +115 -0
  85. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  86. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  87. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  88. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  89. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  90. package/src/qingflow_mcp/__init__.py +1 -1
  91. package/src/qingflow_mcp/__main__.py +6 -2
  92. package/src/qingflow_mcp/builder_facade/models.py +287 -25
  93. package/src/qingflow_mcp/builder_facade/service.py +4195 -856
  94. package/src/qingflow_mcp/cli/commands/builder.py +316 -247
  95. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  96. package/src/qingflow_mcp/cli/commands/common.py +12 -3
  97. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  98. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  99. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  100. package/src/qingflow_mcp/cli/commands/record.py +101 -27
  101. package/src/qingflow_mcp/cli/commands/task.py +28 -47
  102. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  103. package/src/qingflow_mcp/cli/context.py +0 -3
  104. package/src/qingflow_mcp/cli/formatters.py +784 -16
  105. package/src/qingflow_mcp/cli/main.py +117 -33
  106. package/src/qingflow_mcp/errors.py +43 -2
  107. package/src/qingflow_mcp/public_surface.py +26 -17
  108. package/src/qingflow_mcp/response_trim.py +81 -17
  109. package/src/qingflow_mcp/server.py +14 -12
  110. package/src/qingflow_mcp/server_app_builder.py +65 -21
  111. package/src/qingflow_mcp/server_app_user.py +22 -16
  112. package/src/qingflow_mcp/session_store.py +11 -7
  113. package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
  114. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  115. package/src/qingflow_mcp/solution/executor.py +245 -18
  116. package/src/qingflow_mcp/tools/ai_builder_tools.py +1782 -399
  117. package/src/qingflow_mcp/tools/app_tools.py +184 -43
  118. package/src/qingflow_mcp/tools/approval_tools.py +197 -35
  119. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  120. package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
  121. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  122. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  123. package/src/qingflow_mcp/tools/export_tools.py +244 -34
  124. package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
  125. package/src/qingflow_mcp/tools/file_tools.py +9 -3
  126. package/src/qingflow_mcp/tools/import_tools.py +336 -49
  127. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  128. package/src/qingflow_mcp/tools/package_tools.py +118 -6
  129. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  130. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  131. package/src/qingflow_mcp/tools/record_tools.py +1141 -356
  132. package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
  133. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  134. package/src/qingflow_mcp/tools/solution_tools.py +59 -45
  135. package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
  136. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  137. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  138. package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
  139. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
  140. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
  141. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
  142. /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
  143. /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
  144. /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
  145. /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
  146. /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
  147. /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
  148. /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
  149. /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
  150. /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
  151. /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
  152. /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
  153. /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
  154. /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
@@ -1,11 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from typing import Any
4
5
 
5
6
  from mcp.server.fastmcp import FastMCP
6
7
 
7
8
  from ..config import DEFAULT_PROFILE
8
- from ..errors import QingflowApiError, raise_tool_error
9
+ from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error, message_looks_like_invalid_token, raise_tool_error
9
10
  from ..json_types import JSONObject
10
11
  from ..list_type_labels import get_record_list_type_label
11
12
  from .base import ToolBase, tool_cn_name
@@ -172,15 +173,26 @@ class ApprovalTools(ToolBase):
172
173
  keyword: str | None = None,
173
174
  ) -> dict[str, Any]:
174
175
  """执行记录相关逻辑。"""
175
- raw = self.record_comment_mention_candidates(
176
- profile=profile,
177
- app_key=app_key,
178
- apply_id=record_id,
179
- page_size=page_size,
180
- page_num=page_num,
181
- list_type=list_type,
182
- keyword=keyword,
183
- )
176
+ selection = {"app_key": app_key, "record_id": record_id, "list_type": list_type, "keyword": keyword}
177
+ try:
178
+ raw = self.record_comment_mention_candidates(
179
+ profile=profile,
180
+ app_key=app_key,
181
+ apply_id=record_id,
182
+ page_size=page_size,
183
+ page_num=page_num,
184
+ list_type=list_type,
185
+ keyword=keyword,
186
+ )
187
+ except RuntimeError as exc:
188
+ if not _is_optional_approval_runtime_error(exc):
189
+ raise
190
+ return self._runtime_error_as_auxiliary_result(
191
+ exc,
192
+ error_code="RECORD_COMMENT_MENTIONS_UNAVAILABLE",
193
+ selection=selection,
194
+ fallback_hint="Mention candidates are unavailable in this permission context; write a plain comment or retry without mentions.",
195
+ )
184
196
  items = _approval_page_items(raw.get("page"))
185
197
  return self._public_page_response(
186
198
  raw,
@@ -192,7 +204,7 @@ class ApprovalTools(ToolBase):
192
204
  "page_amount": _approval_page_amount(raw.get("page")),
193
205
  "reported_total": _approval_page_total(raw.get("page")),
194
206
  },
195
- selection={"app_key": app_key, "record_id": record_id, "list_type": list_type, "keyword": keyword},
207
+ selection=selection,
196
208
  )
197
209
 
198
210
  @tool_cn_name("任务通过")
@@ -242,13 +254,24 @@ class ApprovalTools(ToolBase):
242
254
  @tool_cn_name("任务退回候选")
243
255
  def task_rollback_candidates(self, *, profile: str, app_key: str, record_id: int, workflow_node_id: int) -> dict[str, Any]:
244
256
  """执行任务相关逻辑。"""
245
- raw = self.record_rollback_candidates(profile=profile, app_key=app_key, apply_id=record_id, audit_node_id=workflow_node_id)
257
+ selection = {"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id}
258
+ try:
259
+ raw = self.record_rollback_candidates(profile=profile, app_key=app_key, apply_id=record_id, audit_node_id=workflow_node_id)
260
+ except RuntimeError as exc:
261
+ if not _is_optional_approval_runtime_error(exc):
262
+ raise
263
+ return self._runtime_error_as_auxiliary_result(
264
+ exc,
265
+ error_code="TASK_ROLLBACK_CANDIDATES_UNAVAILABLE",
266
+ selection=selection,
267
+ fallback_hint="Rollback candidates are unavailable in this permission context; use task get for current context or retry with a valid actionable node.",
268
+ )
246
269
  items = _approval_page_items(raw.get("result"))
247
270
  return self._public_page_response(
248
271
  raw,
249
272
  items=items,
250
273
  pagination={"returned_items": len(items)},
251
- selection={"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id},
274
+ selection=selection,
252
275
  )
253
276
 
254
277
  @tool_cn_name("任务退回")
@@ -331,15 +354,26 @@ class ApprovalTools(ToolBase):
331
354
  keyword: str | None = None,
332
355
  ) -> dict[str, Any]:
333
356
  """执行任务相关逻辑。"""
334
- raw = self.record_transfer_candidates(
335
- profile=profile,
336
- app_key=app_key,
337
- apply_id=record_id,
338
- page_size=page_size,
339
- page_num=page_num,
340
- audit_node_id=workflow_node_id,
341
- keyword=keyword,
342
- )
357
+ selection = {"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id, "keyword": keyword}
358
+ try:
359
+ raw = self.record_transfer_candidates(
360
+ profile=profile,
361
+ app_key=app_key,
362
+ apply_id=record_id,
363
+ page_size=page_size,
364
+ page_num=page_num,
365
+ audit_node_id=workflow_node_id,
366
+ keyword=keyword,
367
+ )
368
+ except RuntimeError as exc:
369
+ if not _is_optional_approval_runtime_error(exc):
370
+ raise
371
+ return self._runtime_error_as_auxiliary_result(
372
+ exc,
373
+ error_code="TASK_TRANSFER_CANDIDATES_UNAVAILABLE",
374
+ selection=selection,
375
+ fallback_hint="Transfer candidates are unavailable in this permission context; use task get for current context or retry with a valid actionable node.",
376
+ )
343
377
  original_items = _approval_page_items(raw.get("page"))
344
378
  items = self._filter_self_transfer_candidates(profile=profile, items=original_items)
345
379
  filtered_count = max(len(original_items) - len(items), 0)
@@ -353,7 +387,7 @@ class ApprovalTools(ToolBase):
353
387
  "page_amount": _approval_page_amount(raw.get("page")),
354
388
  "reported_total": max(_approval_page_total(raw.get("page")) - filtered_count, 0),
355
389
  },
356
- selection={"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id, "keyword": keyword},
390
+ selection=selection,
357
391
  )
358
392
 
359
393
  @tool_cn_name("新增评论")
@@ -763,9 +797,18 @@ class ApprovalTools(ToolBase):
763
797
  node_id = self._extract_node_id(body)
764
798
  body["nodeId"] = self._resolve_actionable_node_id(context, app_key, apply_id, node_id)
765
799
  body["applyId"] = self._match_or_fill_int(body, field_name="applyId", expected_value=apply_id)
766
- body["formId"] = self._resolve_form_id(profile, context, app_key, explicit_form_id=body.get("formId"))
800
+ current_detail: dict[str, Any] | None = None
801
+ if body.get("answers") is None or body.get("formId") is None:
802
+ current_detail = self._fetch_current_todo_detail(context, app_key, apply_id, body["nodeId"])
803
+ body["formId"] = self._resolve_form_id(
804
+ profile,
805
+ context,
806
+ app_key,
807
+ explicit_form_id=body.get("formId"),
808
+ current_detail=current_detail,
809
+ )
767
810
  if body.get("answers") is None:
768
- body["answers"] = self._fetch_current_todo_answers(context, app_key, apply_id, body["nodeId"])
811
+ body["answers"] = self._extract_current_todo_answers(current_detail, apply_id=apply_id, node_id=body["nodeId"])
769
812
 
770
813
  self._validate_approval_payload(body)
771
814
  return body
@@ -782,12 +825,27 @@ class ApprovalTools(ToolBase):
782
825
  raise_tool_error(QingflowApiError.config_error("payload.nodeId or payload.auditNodeId must be a positive integer"))
783
826
  return node_id
784
827
 
785
- def _resolve_form_id(self, profile: str, context, app_key: str, *, explicit_form_id: Any | None) -> int: # type: ignore[no-untyped-def]
828
+ def _resolve_form_id(
829
+ self,
830
+ profile: str,
831
+ context,
832
+ app_key: str,
833
+ *,
834
+ explicit_form_id: Any | None,
835
+ current_detail: dict[str, Any] | None = None,
836
+ ) -> int: # type: ignore[no-untyped-def]
786
837
  """执行内部辅助逻辑。"""
838
+ detail_form_id = self._extract_form_id_from_current_detail(current_detail)
787
839
  if explicit_form_id is not None:
788
840
  if not isinstance(explicit_form_id, int) or explicit_form_id <= 0:
789
841
  raise_tool_error(QingflowApiError.config_error("payload.formId must be a positive integer"))
790
- form_id = self._get_form_id(profile, context, app_key)
842
+ try:
843
+ form_id = detail_form_id or self._get_form_id(profile, context, app_key)
844
+ except QingflowApiError as exc:
845
+ if not _is_optional_approval_precheck_error(exc):
846
+ raise
847
+ self._form_id_cache[f"{profile}:{app_key}"] = explicit_form_id
848
+ return explicit_form_id
791
849
  if form_id != explicit_form_id:
792
850
  raise_tool_error(
793
851
  QingflowApiError.config_error(
@@ -795,6 +853,9 @@ class ApprovalTools(ToolBase):
795
853
  )
796
854
  )
797
855
  return explicit_form_id
856
+ if detail_form_id is not None:
857
+ self._form_id_cache[f"{profile}:{app_key}"] = detail_form_id
858
+ return detail_form_id
798
859
  return self._get_form_id(profile, context, app_key)
799
860
 
800
861
  def _get_form_id(self, profile: str, context, app_key: str) -> int: # type: ignore[no-untyped-def]
@@ -831,12 +892,18 @@ class ApprovalTools(ToolBase):
831
892
 
832
893
  def _resolve_actionable_node_id(self, context, app_key: str, apply_id: int, node_id: int) -> int: # type: ignore[no-untyped-def]
833
894
  """执行内部辅助逻辑。"""
834
- infos = self.backend.request(
835
- "GET",
836
- context,
837
- f"/app/{app_key}/apply/{apply_id}/auditInfo",
838
- params={"type": 1},
839
- )
895
+ try:
896
+ infos = self.backend.request(
897
+ "GET",
898
+ context,
899
+ f"/app/{app_key}/apply/{apply_id}/auditInfo",
900
+ params={"type": 1},
901
+ )
902
+ except QingflowApiError as exc:
903
+ if not _is_optional_approval_precheck_error(exc):
904
+ raise
905
+ self._fetch_current_todo_detail(context, app_key, apply_id, node_id)
906
+ return node_id
840
907
  if not isinstance(infos, list) or not infos:
841
908
  raise_tool_error(
842
909
  QingflowApiError.config_error(
@@ -858,7 +925,7 @@ class ApprovalTools(ToolBase):
858
925
  )
859
926
  return node_id
860
927
 
861
- def _fetch_current_todo_answers(self, context, app_key: str, apply_id: int, node_id: int) -> list[dict[str, Any]]: # type: ignore[no-untyped-def]
928
+ def _fetch_current_todo_detail(self, context, app_key: str, apply_id: int, node_id: int) -> dict[str, Any]: # type: ignore[no-untyped-def]
862
929
  """执行内部辅助逻辑。"""
863
930
  detail = self.backend.request(
864
931
  "GET",
@@ -866,6 +933,21 @@ class ApprovalTools(ToolBase):
866
933
  f"/app/{app_key}/apply/{apply_id}",
867
934
  params={"role": 3, "listType": 1, "auditNodeId": node_id},
868
935
  )
936
+ if not isinstance(detail, dict):
937
+ raise_tool_error(
938
+ QingflowApiError.config_error(
939
+ f"cannot resolve current todo detail for apply_id={apply_id} nodeId={node_id}"
940
+ )
941
+ )
942
+ return detail
943
+
944
+ def _fetch_current_todo_answers(self, context, app_key: str, apply_id: int, node_id: int) -> list[dict[str, Any]]: # type: ignore[no-untyped-def]
945
+ """执行内部辅助逻辑。"""
946
+ detail = self._fetch_current_todo_detail(context, app_key, apply_id, node_id)
947
+ return self._extract_current_todo_answers(detail, apply_id=apply_id, node_id=node_id)
948
+
949
+ def _extract_current_todo_answers(self, detail: dict[str, Any] | None, *, apply_id: int, node_id: int) -> list[dict[str, Any]]:
950
+ """执行内部辅助逻辑。"""
869
951
  answers = detail.get("answers") if isinstance(detail, dict) else None
870
952
  if not isinstance(answers, list):
871
953
  raise_tool_error(
@@ -879,6 +961,21 @@ class ApprovalTools(ToolBase):
879
961
  normalized_answers.append(dict(item))
880
962
  return normalized_answers
881
963
 
964
+ def _extract_form_id_from_current_detail(self, detail: dict[str, Any] | None) -> int | None:
965
+ """执行内部辅助逻辑。"""
966
+ if not isinstance(detail, dict):
967
+ return None
968
+ for key in ("formId", "form_id"):
969
+ value = detail.get(key)
970
+ if isinstance(value, int) and value > 0:
971
+ return value
972
+ form = detail.get("form")
973
+ if isinstance(form, dict):
974
+ value = form.get("formId") or form.get("form_id")
975
+ if isinstance(value, int) and value > 0:
976
+ return value
977
+ return None
978
+
882
979
  def _validate_approval_payload(self, payload: dict[str, Any]) -> None:
883
980
  """执行内部辅助逻辑。"""
884
981
  self._reject_unsupported_fields(payload)
@@ -958,7 +1055,7 @@ class ApprovalTools(ToolBase):
958
1055
  if node_id is None:
959
1056
  node_payload = dict(payload or {})
960
1057
  node_id = self._extract_node_id(node_payload)
961
- delegated = TaskContextTools(self.sessions, self.backend).task_action_execute(
1058
+ delegated = TaskContextTools(self.sessions, self.backend)._task_action_execute_with_locator(
962
1059
  profile=profile,
963
1060
  app_key=app_key,
964
1061
  record_id=record_id,
@@ -995,6 +1092,48 @@ class ApprovalTools(ToolBase):
995
1092
  }
996
1093
  return response
997
1094
 
1095
+ def _runtime_error_as_auxiliary_result(
1096
+ self,
1097
+ error: RuntimeError,
1098
+ *,
1099
+ error_code: str,
1100
+ selection: dict[str, Any],
1101
+ fallback_hint: str,
1102
+ ) -> dict[str, Any]:
1103
+ """Return a structured failure for optional approval/comment helpers."""
1104
+ try:
1105
+ payload = json.loads(str(error))
1106
+ except json.JSONDecodeError:
1107
+ payload = {"message": str(error)}
1108
+ details = payload.get("details") if isinstance(payload.get("details"), dict) else {}
1109
+ warning: dict[str, Any] = {
1110
+ "code": error_code,
1111
+ "message": fallback_hint,
1112
+ }
1113
+ for key in ("category", "backend_code", "request_id", "http_status"):
1114
+ if payload.get(key) is not None:
1115
+ warning[key] = payload.get(key)
1116
+ response: dict[str, Any] = {
1117
+ "ok": False,
1118
+ "status": "failed",
1119
+ "error_code": details.get("error_code") or error_code,
1120
+ "message": payload.get("message") or str(error),
1121
+ "warnings": [warning],
1122
+ "output_profile": "normal",
1123
+ "data": {
1124
+ "items": [],
1125
+ "pagination": {"returned_items": 0},
1126
+ "selection": selection,
1127
+ "fallback_hint": fallback_hint,
1128
+ },
1129
+ }
1130
+ for key in ("category", "backend_code", "request_id", "http_status"):
1131
+ if payload.get(key) is not None:
1132
+ response[key] = payload.get(key)
1133
+ if details:
1134
+ response["details"] = details
1135
+ return response
1136
+
998
1137
  def _public_action_response(
999
1138
  self,
1000
1139
  raw: dict[str, Any],
@@ -1060,3 +1199,26 @@ def _approval_page_total(payload: Any) -> Any:
1060
1199
  if isinstance(payload, dict):
1061
1200
  return payload.get("total", payload.get("count"))
1062
1201
  return None
1202
+
1203
+
1204
+ def _is_optional_approval_precheck_error(error: QingflowApiError) -> bool:
1205
+ if is_auth_like_error(error):
1206
+ return False
1207
+ backend_code = backend_code_int(error)
1208
+ return backend_code in {40002, 40027, 404} or error.http_status == 404
1209
+
1210
+
1211
+ def _is_optional_approval_runtime_error(error: RuntimeError) -> bool:
1212
+ try:
1213
+ payload = json.loads(str(error))
1214
+ except json.JSONDecodeError:
1215
+ return False
1216
+ if not isinstance(payload, dict):
1217
+ return False
1218
+ if str(payload.get("category") or "").strip().lower() == "auth":
1219
+ return False
1220
+ if message_looks_like_invalid_token(payload.get("message")):
1221
+ return False
1222
+ if backend_code_value_int(payload.get("http_status")) == 401:
1223
+ return False
1224
+ return backend_code_value_int(payload.get("backend_code")) in {40002, 40027, 404} or backend_code_value_int(payload.get("http_status")) == 404
@@ -14,7 +14,7 @@ from ..config import (
14
14
  get_mcporter_config_path,
15
15
  normalize_base_url,
16
16
  )
17
- from ..errors import QingflowApiError, raise_tool_error
17
+ from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
18
18
  from ..session_store import SessionStore
19
19
  from .base import ToolBase, tool_cn_name
20
20
 
@@ -650,7 +650,9 @@ class AuthTools(ToolBase):
650
650
  qf_version=qf_version,
651
651
  qf_version_source=qf_version_source,
652
652
  )
653
- except QingflowApiError:
653
+ except QingflowApiError as exc:
654
+ if is_auth_like_error(exc):
655
+ raise
654
656
  return None, None
655
657
 
656
658
  def _fetch_first_workspace(
@@ -765,7 +767,9 @@ class AuthTools(ToolBase):
765
767
  qf_version=session_profile.qf_version,
766
768
  qf_version_source=session_profile.qf_version_source,
767
769
  )
768
- except QingflowApiError:
770
+ except QingflowApiError as exc:
771
+ if is_auth_like_error(exc):
772
+ raise
769
773
  return None
770
774
 
771
775
  ws_name = session_profile.selected_ws_name
@@ -823,6 +827,18 @@ class AuthTools(ToolBase):
823
827
  )
824
828
  payload = dict(default_payload)
825
829
  payload["permission_level"] = permission_level
830
+ warnings: list[dict[str, Any]] = []
831
+ if permission_level is None:
832
+ warnings.append(
833
+ {
834
+ "code": "WORKSPACE_PERMISSION_LEVEL_UNAVAILABLE",
835
+ "message": (
836
+ "auth_whoami could not resolve the selected workspace permission level; "
837
+ "do not infer the user is a basic member or an administrator from this null value."
838
+ ),
839
+ "ws_id": ws_id,
840
+ }
841
+ )
826
842
 
827
843
  context = BackendRequestContext(
828
844
  base_url=backend_session.base_url,
@@ -831,14 +847,17 @@ class AuthTools(ToolBase):
831
847
  qf_version=backend_session.qf_version,
832
848
  qf_version_source=backend_session.qf_version_source,
833
849
  )
850
+ member_lookup_warnings: list[dict[str, Any]] = []
834
851
  member = self._lookup_current_member(
835
852
  context=context,
836
853
  uid=session_profile.uid,
837
854
  email=session_profile.email,
838
855
  nick_name=session_profile.nick_name,
856
+ warnings=member_lookup_warnings,
839
857
  )
858
+ warnings.extend(member_lookup_warnings)
840
859
  if member is None:
841
- return payload, [
860
+ warnings.append(
842
861
  {
843
862
  "code": "CURRENT_MEMBER_PROFILE_UNAVAILABLE",
844
863
  "message": (
@@ -846,11 +865,12 @@ class AuthTools(ToolBase):
846
865
  f"in workspace {ws_id}."
847
866
  ),
848
867
  }
849
- ]
868
+ )
869
+ return payload, warnings
850
870
 
851
871
  payload["departments"] = self._compact_departments(member)
852
872
  payload["roles"] = self._compact_roles(member)
853
- return payload, []
873
+ return payload, warnings
854
874
 
855
875
  def _workspace_permission_level(
856
876
  self,
@@ -882,7 +902,9 @@ class AuthTools(ToolBase):
882
902
  """执行内部辅助逻辑。"""
883
903
  try:
884
904
  workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
885
- except QingflowApiError:
905
+ except QingflowApiError as exc:
906
+ if not _is_optional_auth_lookup_error(exc):
907
+ raise
886
908
  return None
887
909
  if not isinstance(workspace, dict):
888
910
  return None
@@ -897,7 +919,9 @@ class AuthTools(ToolBase):
897
919
  "/user/workspaceList/pageQuery",
898
920
  json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2, 3]},
899
921
  )
900
- except QingflowApiError:
922
+ except QingflowApiError as exc:
923
+ if not _is_optional_auth_lookup_error(exc):
924
+ raise
901
925
  return None
902
926
  workspaces = payload.get("list") if isinstance(payload, dict) else []
903
927
  if not isinstance(workspaces, list):
@@ -915,20 +939,21 @@ class AuthTools(ToolBase):
915
939
  uid: int | None,
916
940
  email: str | None,
917
941
  nick_name: str | None,
942
+ warnings: list[dict[str, Any]] | None = None,
918
943
  ) -> dict[str, Any] | None:
919
944
  """执行内部辅助逻辑。"""
920
945
  candidates: list[dict[str, Any]] = []
921
946
  for keyword in (email, nick_name):
922
- member = self._search_member_once(context, uid=uid, keyword=keyword)
947
+ member = self._search_member_once(context, uid=uid, keyword=keyword, warnings=warnings)
923
948
  if member is not None:
924
949
  return member
925
950
  if keyword:
926
- candidates.extend(self._search_member_items(context, keyword=keyword))
951
+ candidates.extend(self._search_member_items(context, keyword=keyword, warnings=warnings))
927
952
  if uid is not None and uid > 0:
928
953
  for item in candidates:
929
954
  if self._same_member(item, uid=uid):
930
955
  return item
931
- return self._search_member_once(context, uid=uid, keyword=None)
956
+ return self._search_member_once(context, uid=uid, keyword=None, warnings=warnings)
932
957
  return None
933
958
 
934
959
  def _search_member_once(
@@ -937,14 +962,21 @@ class AuthTools(ToolBase):
937
962
  *,
938
963
  uid: int | None,
939
964
  keyword: str | None,
965
+ warnings: list[dict[str, Any]] | None = None,
940
966
  ) -> dict[str, Any] | None:
941
967
  """执行内部辅助逻辑。"""
942
- for item in self._search_member_items(context, keyword=keyword):
968
+ for item in self._search_member_items(context, keyword=keyword, warnings=warnings):
943
969
  if self._same_member(item, uid=uid):
944
970
  return item
945
971
  return None
946
972
 
947
- def _search_member_items(self, context: BackendRequestContext, *, keyword: str | None) -> list[dict[str, Any]]:
973
+ def _search_member_items(
974
+ self,
975
+ context: BackendRequestContext,
976
+ *,
977
+ keyword: str | None,
978
+ warnings: list[dict[str, Any]] | None = None,
979
+ ) -> list[dict[str, Any]]:
948
980
  """执行内部辅助逻辑。"""
949
981
  params: dict[str, Any] = {"pageNum": 1, "pageSize": 100, "containDisable": True}
950
982
  normalized_keyword = str(keyword or "").strip()
@@ -952,11 +984,38 @@ class AuthTools(ToolBase):
952
984
  params["keyword"] = normalized_keyword
953
985
  try:
954
986
  payload = self.backend.request("GET", context, "/contact", params=params)
955
- except QingflowApiError:
987
+ except QingflowApiError as exc:
988
+ if not _is_optional_auth_lookup_error(exc):
989
+ raise
990
+ if warnings is not None and _is_contact_directory_permission_denied(exc):
991
+ self._append_unique_warning(warnings, self._contact_directory_permission_warning(exc))
956
992
  return []
957
993
  items = self._extract_items(payload)
958
994
  return [item for item in items if isinstance(item, dict)]
959
995
 
996
+ def _append_unique_warning(self, warnings: list[dict[str, Any]], warning: dict[str, Any]) -> None:
997
+ """执行内部辅助逻辑。"""
998
+ code = self._normalize_text(warning.get("code"))
999
+ if code is not None and any(item.get("code") == code for item in warnings):
1000
+ return
1001
+ warnings.append(warning)
1002
+
1003
+ def _contact_directory_permission_warning(self, error: QingflowApiError) -> dict[str, Any]:
1004
+ """执行内部辅助逻辑。"""
1005
+ warning: dict[str, Any] = {
1006
+ "code": "CONTACT_DIRECTORY_PERMISSION_DENIED",
1007
+ "message": (
1008
+ "auth_whoami could not read current member departments and roles because "
1009
+ "the contact directory is not readable in this permission context; "
1010
+ "permission_level still comes from the workspace auth route."
1011
+ ),
1012
+ "category": error.category,
1013
+ "backend_code": backend_code_int(error),
1014
+ "http_status": error.http_status,
1015
+ "request_id": error.request_id,
1016
+ }
1017
+ return {key: value for key, value in warning.items() if value is not None}
1018
+
960
1019
  def _same_member(self, item: dict[str, Any], *, uid: int | None) -> bool:
961
1020
  """执行内部辅助逻辑。"""
962
1021
  if uid is None or uid <= 0:
@@ -1105,7 +1164,9 @@ class AuthTools(ToolBase):
1105
1164
  qf_version=qf_version,
1106
1165
  qf_version_source=qf_version_source,
1107
1166
  )
1108
- except QingflowApiError:
1167
+ except QingflowApiError as exc:
1168
+ if is_auth_like_error(exc):
1169
+ raise
1109
1170
  workspace = None
1110
1171
  if isinstance(workspace, dict):
1111
1172
  workspace_name = str(workspace.get("workspaceName") or workspace.get("wsName") or "").strip()
@@ -1119,7 +1180,9 @@ class AuthTools(ToolBase):
1119
1180
  qf_version=qf_version,
1120
1181
  qf_version_source=qf_version_source,
1121
1182
  )
1122
- except QingflowApiError:
1183
+ except QingflowApiError as exc:
1184
+ if is_auth_like_error(exc):
1185
+ raise
1123
1186
  fallback = None
1124
1187
  return fallback or workspace
1125
1188
 
@@ -1157,3 +1220,16 @@ class AuthTools(ToolBase):
1157
1220
  None,
1158
1221
  )
1159
1222
  return found if isinstance(found, dict) else None
1223
+
1224
+
1225
+ def _is_contact_directory_permission_denied(error: QingflowApiError) -> bool:
1226
+ if is_auth_like_error(error):
1227
+ return False
1228
+ return backend_code_int(error) in {40002, 40027}
1229
+
1230
+
1231
+ def _is_optional_auth_lookup_error(error: QingflowApiError) -> bool:
1232
+ if is_auth_like_error(error):
1233
+ return False
1234
+ backend_code = backend_code_int(error)
1235
+ return backend_code in {40002, 40027, 404} or error.http_status == 404