@josephyan/qingflow-cli 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +7 -3
  2. package/docs/local-agent-install.md +57 -6
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/bin/qingflow.mjs +1 -34
  6. package/npm/lib/runtime.mjs +21 -101
  7. package/npm/scripts/postinstall.mjs +1 -10
  8. package/package.json +3 -2
  9. package/pyproject.toml +1 -1
  10. package/skills/qingflow-cli/SKILL.md +58 -44
  11. package/skills/qingflow-cli/manifest.yaml +1 -1
  12. package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
  13. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
  14. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
  15. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
  16. package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
  17. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
  18. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
  19. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
  21. package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
  22. package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
  23. package/skills/qingflow-cli/reference/builder/README.md +41 -0
  24. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
  25. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
  26. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
  27. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
  28. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
  29. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
  30. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
  31. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
  32. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
  33. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
  34. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
  35. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
  36. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
  37. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
  38. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
  39. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
  40. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
  41. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
  42. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
  43. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
  44. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
  45. package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
  46. package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
  47. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
  48. package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
  49. package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
  50. package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
  51. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
  52. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
  53. package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
  54. package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
  55. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
  56. package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
  57. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
  58. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
  59. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
  60. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
  61. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
  62. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
  63. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
  64. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
  65. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
  66. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
  67. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
  68. package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
  69. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
  70. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
  71. package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
  72. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
  73. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
  74. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
  75. package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
  76. package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
  77. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
  78. package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
  79. package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
  80. package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
  81. package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
  82. package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
  83. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
  84. package/skills/qingflow-mcp-setup/SKILL.md +115 -0
  85. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  86. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  87. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  88. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  89. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  90. package/src/qingflow_mcp/__init__.py +1 -1
  91. package/src/qingflow_mcp/__main__.py +6 -2
  92. package/src/qingflow_mcp/builder_facade/models.py +282 -102
  93. package/src/qingflow_mcp/builder_facade/service.py +4192 -935
  94. package/src/qingflow_mcp/cli/commands/builder.py +316 -298
  95. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  96. package/src/qingflow_mcp/cli/commands/common.py +12 -3
  97. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  98. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  99. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  100. package/src/qingflow_mcp/cli/commands/record.py +101 -27
  101. package/src/qingflow_mcp/cli/commands/task.py +28 -47
  102. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  103. package/src/qingflow_mcp/cli/context.py +0 -3
  104. package/src/qingflow_mcp/cli/formatters.py +784 -16
  105. package/src/qingflow_mcp/cli/main.py +117 -33
  106. package/src/qingflow_mcp/errors.py +43 -2
  107. package/src/qingflow_mcp/public_surface.py +26 -17
  108. package/src/qingflow_mcp/response_trim.py +81 -17
  109. package/src/qingflow_mcp/server.py +14 -12
  110. package/src/qingflow_mcp/server_app_builder.py +65 -21
  111. package/src/qingflow_mcp/server_app_user.py +22 -16
  112. package/src/qingflow_mcp/session_store.py +11 -7
  113. package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
  114. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  115. package/src/qingflow_mcp/solution/executor.py +245 -18
  116. package/src/qingflow_mcp/tools/ai_builder_tools.py +1780 -406
  117. package/src/qingflow_mcp/tools/app_tools.py +184 -43
  118. package/src/qingflow_mcp/tools/approval_tools.py +197 -35
  119. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  120. package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
  121. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  122. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  123. package/src/qingflow_mcp/tools/export_tools.py +244 -34
  124. package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
  125. package/src/qingflow_mcp/tools/file_tools.py +9 -3
  126. package/src/qingflow_mcp/tools/import_tools.py +336 -49
  127. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  128. package/src/qingflow_mcp/tools/package_tools.py +118 -6
  129. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  130. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  131. package/src/qingflow_mcp/tools/record_tools.py +1141 -356
  132. package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
  133. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  134. package/src/qingflow_mcp/tools/solution_tools.py +59 -45
  135. package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
  136. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  137. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  138. package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
  139. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
  140. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
  141. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
  142. /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
  143. /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
  144. /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
  145. /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
  146. /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
  147. /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
  148. /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
  149. /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
  150. /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
  151. /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
  152. /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
  153. /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
  154. /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
@@ -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_value_int, message_looks_like_invalid_token, raise_tool_error
9
10
  from ..list_type_labels import get_record_list_type_label, get_task_type_label
10
11
  from .base import ToolBase, tool_cn_name
11
12
 
@@ -150,7 +151,17 @@ class TaskTools(ToolBase):
150
151
  app_key: str | None,
151
152
  ) -> dict[str, Any]:
152
153
  """执行任务相关逻辑。"""
153
- raw = self.task_statistics(profile=profile, app_key=app_key)
154
+ try:
155
+ raw = self.task_statistics(profile=profile, app_key=app_key)
156
+ except RuntimeError as exc:
157
+ if not _is_optional_task_runtime_error(exc):
158
+ raise
159
+ return self._runtime_error_as_auxiliary_result(
160
+ exc,
161
+ error_code="TASK_SUMMARY_UNAVAILABLE",
162
+ selection={"app_key": app_key},
163
+ fallback_hint="Use task list/get directly; task summary is an auxiliary statistics entrypoint.",
164
+ )
154
165
  statistics = raw.get("statistics", {})
155
166
  summary = self._normalize_task_summary_payload(statistics)
156
167
  return {
@@ -243,34 +254,51 @@ class TaskTools(ToolBase):
243
254
  if limit <= 0:
244
255
  raise_tool_error(QingflowApiError.config_error("limit must be positive"))
245
256
 
246
- if dimension == "worksheet":
247
- raw = self.task_worksheet_statistics(
248
- profile=profile,
249
- type=normalized_type,
250
- worksheet_name=query,
251
- page_num=1,
252
- page_size=max(limit, 20),
253
- )
254
- source = raw.get("page", {})
255
- elif app_key:
256
- raw = self.task_node_statistics(
257
- profile=profile,
258
- app_key=app_key,
259
- type=normalized_type,
260
- search_key=query,
261
- )
262
- source = raw.get("nodes", {})
263
- else:
264
- raw = self.task_workflow_nodes(
265
- profile=profile,
266
- type=normalized_type,
267
- status=str(normalized_status),
268
- app_key_list=None,
269
- search_key=query,
270
- page_num=1,
271
- page_size=max(limit, 20),
257
+ try:
258
+ if dimension == "worksheet":
259
+ raw = self.task_worksheet_statistics(
260
+ profile=profile,
261
+ type=normalized_type,
262
+ worksheet_name=query,
263
+ page_num=1,
264
+ page_size=max(limit, 20),
265
+ )
266
+ source = raw.get("page", {})
267
+ elif app_key:
268
+ raw = self.task_node_statistics(
269
+ profile=profile,
270
+ app_key=app_key,
271
+ type=normalized_type,
272
+ search_key=query,
273
+ )
274
+ source = raw.get("nodes", {})
275
+ else:
276
+ raw = self.task_workflow_nodes(
277
+ profile=profile,
278
+ type=normalized_type,
279
+ status=str(normalized_status),
280
+ app_key_list=None,
281
+ search_key=query,
282
+ page_num=1,
283
+ page_size=max(limit, 20),
284
+ )
285
+ source = raw.get("page", {})
286
+ except RuntimeError as exc:
287
+ if not _is_optional_task_runtime_error(exc):
288
+ raise
289
+ return self._runtime_error_as_auxiliary_result(
290
+ exc,
291
+ error_code="TASK_FACETS_UNAVAILABLE",
292
+ selection={
293
+ "task_box": task_box,
294
+ "flow_status": flow_status,
295
+ "dimension": dimension,
296
+ "app_key": app_key,
297
+ "query": query,
298
+ "limit": limit,
299
+ },
300
+ fallback_hint="Use task list/get directly; task facets are an auxiliary grouping entrypoint.",
272
301
  )
273
- source = raw.get("page", {})
274
302
 
275
303
  groups = self._normalize_task_facets(source)
276
304
  rows_truncated = len(groups) > limit
@@ -824,6 +852,46 @@ class TaskTools(ToolBase):
824
852
  groups.append({"key": key if key is not None else label, "label": label, "count": count})
825
853
  return groups
826
854
 
855
+ def _runtime_error_as_auxiliary_result(
856
+ self,
857
+ error: RuntimeError,
858
+ *,
859
+ error_code: str,
860
+ selection: dict[str, Any],
861
+ fallback_hint: str,
862
+ ) -> dict[str, Any]:
863
+ """Return a structured failure for optional task discovery helpers."""
864
+ try:
865
+ payload = json.loads(str(error))
866
+ except json.JSONDecodeError:
867
+ payload = {"message": str(error)}
868
+ details = payload.get("details") if isinstance(payload.get("details"), dict) else {}
869
+ warning: dict[str, Any] = {
870
+ "code": error_code,
871
+ "message": fallback_hint,
872
+ }
873
+ for key in ("category", "backend_code", "request_id", "http_status"):
874
+ if payload.get(key) is not None:
875
+ warning[key] = payload.get(key)
876
+ response: dict[str, Any] = {
877
+ "ok": False,
878
+ "status": "failed",
879
+ "error_code": details.get("error_code") or error_code,
880
+ "message": payload.get("message") or str(error),
881
+ "warnings": [warning],
882
+ "output_profile": "normal",
883
+ "data": {
884
+ "selection": selection,
885
+ "fallback_hint": fallback_hint,
886
+ },
887
+ }
888
+ for key in ("category", "backend_code", "request_id", "http_status"):
889
+ if payload.get(key) is not None:
890
+ response[key] = payload.get(key)
891
+ if details:
892
+ response["details"] = details
893
+ return response
894
+
827
895
  def _public_task_action_response(
828
896
  self,
829
897
  raw: dict[str, Any],
@@ -859,6 +927,22 @@ class TaskTools(ToolBase):
859
927
  }
860
928
 
861
929
 
930
+ def _is_optional_task_runtime_error(error: RuntimeError) -> bool:
931
+ try:
932
+ payload = json.loads(str(error))
933
+ except json.JSONDecodeError:
934
+ return False
935
+ if not isinstance(payload, dict):
936
+ return False
937
+ if str(payload.get("category") or "").strip().lower() == "auth":
938
+ return False
939
+ if message_looks_like_invalid_token(payload.get("message")):
940
+ return False
941
+ if backend_code_value_int(payload.get("http_status")) == 401:
942
+ return False
943
+ return backend_code_value_int(payload.get("backend_code")) in {40002, 40027, 404} or backend_code_value_int(payload.get("http_status")) == 404
944
+
945
+
862
946
  def _task_page_items(payload: Any) -> list[Any]:
863
947
  if isinstance(payload, list):
864
948
  return payload
@@ -3,8 +3,9 @@ from __future__ import annotations
3
3
  from mcp.server.fastmcp import FastMCP
4
4
 
5
5
  from ..config import DEFAULT_PROFILE
6
- from ..errors import QingflowApiError, raise_tool_error
6
+ from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
7
7
  from ..json_types import JSONObject, JSONValue
8
+ from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS
8
9
  from .base import ToolBase, tool_cn_name
9
10
 
10
11
 
@@ -60,7 +61,31 @@ class ViewTools(ToolBase):
60
61
  def view_list(self, *, profile: str, app_key: str) -> JSONObject:
61
62
  """执行视图相关逻辑。"""
62
63
  self._require_app_key(app_key)
63
- return self._request(profile, "GET", f"/app/{app_key}/view/viewList", app_key=app_key)
64
+
65
+ def runner(session_profile, context):
66
+ warnings: list[JSONObject] = []
67
+ verification: JSONObject = {"custom_views_loaded": True, "system_view_fallback_used": False}
68
+ try:
69
+ result = self.backend.request("GET", context, f"/app/{app_key}/view/viewList")
70
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result}
71
+ except QingflowApiError as exc:
72
+ if not _is_optional_view_list_error(exc):
73
+ raise
74
+ items, probe_warnings = self._resolve_accessible_system_views(context, app_key)
75
+ warnings.extend(probe_warnings)
76
+ warnings.append(_view_list_fallback_warning("VIEW_LIST_UNAVAILABLE", exc))
77
+ verification = {"custom_views_loaded": False, "system_view_fallback_used": True}
78
+ return {
79
+ "profile": profile,
80
+ "ws_id": session_profile.selected_ws_id,
81
+ "app_key": app_key,
82
+ "result": items,
83
+ "items": items,
84
+ "warnings": warnings,
85
+ "verification": verification,
86
+ }
87
+
88
+ return self._run(profile, runner)
64
89
 
65
90
  @tool_cn_name("视图列表(扁平)")
66
91
  def view_list_flat(self, *, profile: str, app_key: str) -> JSONObject:
@@ -107,7 +132,40 @@ class ViewTools(ToolBase):
107
132
  """执行视图相关逻辑。"""
108
133
  self._require_viewgraph_key(viewgraph_key)
109
134
  params = {"pass": passcode} if passcode else None
110
- return self._request(profile, "GET", f"/view/{viewgraph_key}/viewConfig/baseInfo", viewgraph_key=viewgraph_key, params=params)
135
+ def runner(session_profile, context):
136
+ warnings: list[JSONObject] = []
137
+ verification: JSONObject = {"base_info_verified": True, "fallback_config_used": False}
138
+ try:
139
+ result = self.backend.request("GET", context, f"/view/{viewgraph_key}/viewConfig/baseInfo", params=params)
140
+ except QingflowApiError as error:
141
+ if not _is_optional_view_list_error(error):
142
+ raise
143
+ result = self.backend.request("GET", context, f"/view/{viewgraph_key}/viewConfig")
144
+ verification["base_info_verified"] = False
145
+ verification["fallback_config_used"] = True
146
+ warning: JSONObject = {
147
+ "code": "VIEW_BASE_INFO_UNAVAILABLE",
148
+ "message": "view_get_base_info used viewConfig because view baseInfo is unavailable in this permission context.",
149
+ }
150
+ if error.backend_code is not None:
151
+ warning["backend_code"] = error.backend_code
152
+ if error.http_status is not None:
153
+ warning["http_status"] = error.http_status
154
+ if error.request_id is not None:
155
+ warning["request_id"] = error.request_id
156
+ if error.details:
157
+ warning["details"] = error.details
158
+ warnings.append(warning)
159
+ return {
160
+ "profile": profile,
161
+ "ws_id": session_profile.selected_ws_id,
162
+ "viewgraph_key": viewgraph_key,
163
+ "result": result,
164
+ "warnings": warnings,
165
+ "verification": verification,
166
+ }
167
+
168
+ return self._run(profile, runner)
111
169
 
112
170
  @tool_cn_name("更新视图配置")
113
171
  def view_update(self, *, profile: str, viewgraph_key: str, payload: JSONObject) -> JSONObject:
@@ -303,6 +361,34 @@ class ViewTools(ToolBase):
303
361
  if value <= 0:
304
362
  raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))
305
363
 
364
+ def _resolve_accessible_system_views(self, context, app_key: str) -> tuple[list[JSONObject], list[JSONObject]]:
365
+ items: list[JSONObject] = []
366
+ warnings: list[JSONObject] = []
367
+ for view_id, list_type, name in SYSTEM_VIEW_DEFINITIONS:
368
+ try:
369
+ self.backend.request(
370
+ "POST",
371
+ context,
372
+ f"/app/{app_key}/apply/filter",
373
+ json_body={"type": list_type, "pageNum": 1, "pageSize": 1},
374
+ )
375
+ except QingflowApiError as exc:
376
+ if _is_optional_view_list_error(exc):
377
+ continue
378
+ raise
379
+ items.append(
380
+ {
381
+ "view_id": view_id,
382
+ "viewId": view_id,
383
+ "viewName": name,
384
+ "name": name,
385
+ "kind": "system",
386
+ "list_type": list_type,
387
+ "analysis_supported": True,
388
+ }
389
+ )
390
+ return items, warnings
391
+
306
392
  def _normalize_reorder_payload(self, payload: list[JSONObject]) -> list[JSONObject]:
307
393
  """执行内部辅助逻辑。"""
308
394
  first_item = payload[0]
@@ -333,3 +419,20 @@ class ViewTools(ToolBase):
333
419
  if not view_keys:
334
420
  raise_tool_error(QingflowApiError.config_error("payload must include viewgraphKey/viewKey or ordinalType/viewKeyList"))
335
421
  return [{"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": view_keys}]
422
+
423
+
424
+ def _is_optional_view_list_error(error: QingflowApiError) -> bool:
425
+ if is_auth_like_error(error):
426
+ return False
427
+ backend_code = backend_code_int(error)
428
+ return backend_code in {40002, 40027, 404} or error.http_status == 404
429
+
430
+
431
+ def _view_list_fallback_warning(code: str, error: QingflowApiError) -> JSONObject:
432
+ return {
433
+ "code": code,
434
+ "message": "custom view list is unavailable; returned accessible system views instead",
435
+ "backend_code": error.backend_code,
436
+ "http_status": error.http_status,
437
+ "request_id": error.request_id,
438
+ }
@@ -14,13 +14,25 @@ class WorkflowTools(ToolBase):
14
14
 
15
15
  类型:流程配置工具。
16
16
  主要职责:
17
- 1. 支持流程运行时校验与调试辅助能力;
18
- 2. 保留 legacy 节点写路径供内部/测试直接调用(非 MCP 公开)。
19
- 设计期流程配置读取统一走 WorkflowSpec(app_flow_get / GET /api/workflow/spec)。
17
+ 1. 查询流程节点与流程图配置;
18
+ 2. 读取与更新流程规则;
19
+ 3. 支持流程发布与流程调试辅助能力。
20
20
  """
21
21
 
22
22
  def register(self, mcp: FastMCP) -> None:
23
23
  """注册当前工具到 MCP 服务。"""
24
+ @mcp.tool()
25
+ def workflow_list_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
26
+ return self.workflow_list_nodes(profile=profile, app_key=app_key)
27
+
28
+ @mcp.tool()
29
+ def workflow_get_node_detail(profile: str = DEFAULT_PROFILE, app_key: str = "", audit_node_id: int = 0) -> JSONObject:
30
+ return self.workflow_get_node_detail(profile=profile, app_key=app_key, audit_node_id=audit_node_id)
31
+
32
+ @mcp.tool()
33
+ def workflow_get_global_settings(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
34
+ return self.workflow_get_global_settings(profile=profile, app_key=app_key)
35
+
24
36
  @mcp.tool()
25
37
  def workflow_get_future_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> JSONObject:
26
38
  return self.workflow_get_future_nodes(profile=profile, app_key=app_key, apply_id=apply_id)
@@ -41,6 +53,22 @@ class WorkflowTools(ToolBase):
41
53
  audit_node_id=audit_node_id,
42
54
  )
43
55
 
56
+ @mcp.tool()
57
+ def workflow_get_qsource_active(profile: str = DEFAULT_PROFILE, app_key: str = "", qsource_id: int = 0) -> JSONObject:
58
+ return self.workflow_get_qsource_active(profile=profile, app_key=app_key, qsource_id=qsource_id)
59
+
60
+ @mcp.tool()
61
+ def workflow_get_qsource_passive(profile: str = DEFAULT_PROFILE, app_key: str = "", qsource_id: int = 0) -> JSONObject:
62
+ return self.workflow_get_qsource_passive(profile=profile, app_key=app_key, qsource_id=qsource_id)
63
+
64
+ @mcp.tool()
65
+ def workflow_get_editable_question_ids(profile: str = DEFAULT_PROFILE, app_key: str = "", audit_node_id: int = 0) -> JSONObject:
66
+ return self.workflow_get_editable_question_ids(profile=profile, app_key=app_key, audit_node_id=audit_node_id)
67
+
68
+ @mcp.tool()
69
+ def workflow_get_print_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
70
+ return self.workflow_get_print_nodes(profile=profile, app_key=app_key)
71
+
44
72
  @tool_cn_name("流程节点列表")
45
73
  def workflow_list_nodes(self, *, profile: str, app_key: str) -> JSONObject:
46
74
  """执行流程相关逻辑。"""
@@ -323,7 +351,23 @@ class WorkflowTools(ToolBase):
323
351
  for candidate_path in paths:
324
352
  try:
325
353
  result = self.backend.request("POST", call_context, candidate_path, json_body=json_body)
326
- response: JSONObject = {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}
354
+ fallback_applied = candidate_path != path or call_context is not context
355
+ response: JSONObject = {
356
+ "profile": profile,
357
+ "ws_id": session_profile.selected_ws_id,
358
+ "result": result,
359
+ "request_path": candidate_path,
360
+ "requested_path": path,
361
+ "fallback_applied": fallback_applied,
362
+ "qf_version_source": (
363
+ call_context.qf_version_source
364
+ or ("context" if call_context.qf_version else "unset")
365
+ ),
366
+ "verification": {
367
+ "primary_path_used": candidate_path == path,
368
+ "qf_version_used": call_context.qf_version is not None,
369
+ },
370
+ }
327
371
  response.update(extra)
328
372
  if risk_operation and risk_target:
329
373
  return self._attach_human_review_notice(response, operation=risk_operation, target=risk_target)
@@ -6,10 +6,13 @@ from mcp.server.fastmcp import FastMCP
6
6
 
7
7
  from ..backend_client import BackendRequestContext
8
8
  from ..config import DEFAULT_PROFILE
9
- from ..errors import QingflowApiError, raise_tool_error
9
+ from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
10
10
  from .base import ToolBase, tool_cn_name
11
11
 
12
12
 
13
+ _WORKSPACE_DETAIL_WARNING_KEY = "_qingflow_workspace_detail_warning"
14
+
15
+
13
16
  class WorkspaceTools(ToolBase):
14
17
  """工作区工具(中文名:工作区与插件管理)。
15
18
 
@@ -91,10 +94,14 @@ class WorkspaceTools(ToolBase):
91
94
  normalized = result if isinstance(result, dict) else {"list": result if isinstance(result, list) else []}
92
95
  workspace_list = normalized.get("list") if isinstance(normalized.get("list"), list) else []
93
96
  selected_ws_id = _.selected_ws_id if _ is not None else None
97
+ warnings: list[dict[str, Any]] = []
94
98
  if selected_ws_id and not any(isinstance(item, dict) and item.get("wsId") == selected_ws_id for item in workspace_list):
95
99
  try:
96
100
  selected_workspace = self.backend.request("GET", context, f"/user/workspace/{selected_ws_id}")
97
- except QingflowApiError:
101
+ except QingflowApiError as exc:
102
+ if not _is_optional_workspace_detail_error(exc):
103
+ raise
104
+ warnings.append(_workspace_detail_fallback_warning(exc, ws_id=selected_ws_id))
98
105
  selected_workspace = {
99
106
  "wsId": selected_ws_id,
100
107
  "workspaceName": _.selected_ws_name if _ is not None else None,
@@ -109,6 +116,14 @@ class WorkspaceTools(ToolBase):
109
116
  "profile": profile,
110
117
  "include_external": include_external,
111
118
  "page": normalized,
119
+ "warnings": warnings,
120
+ "verification": {
121
+ "selected_workspace_detail_verified": not warnings,
122
+ "selected_workspace_included": bool(
123
+ selected_ws_id
124
+ and any(isinstance(item, dict) and item.get("wsId") == selected_ws_id for item in workspace_list)
125
+ ),
126
+ },
112
127
  }
113
128
 
114
129
  return self._run(profile, runner, require_workspace=False)
@@ -127,6 +142,7 @@ class WorkspaceTools(ToolBase):
127
142
  if target_ws_id is None or target_ws_id <= 0:
128
143
  raise_tool_error(QingflowApiError.workspace_not_selected(profile))
129
144
  workspace = self._fetch_workspace_with_fallback(context, ws_id=target_ws_id)
145
+ workspace, warnings = _strip_workspace_detail_warning(workspace)
130
146
  system_version = self._workspace_system_version(workspace)
131
147
  return {
132
148
  "profile": profile,
@@ -134,6 +150,11 @@ class WorkspaceTools(ToolBase):
134
150
  "qf_version": system_version,
135
151
  "qf_version_source": "workspace_system_version" if system_version else "unverified",
136
152
  "workspace": workspace,
153
+ "warnings": warnings,
154
+ "verification": {
155
+ "workspace_detail_verified": not warnings,
156
+ "workspace_list_fallback_used": bool(warnings),
157
+ },
137
158
  }
138
159
 
139
160
  return self._run(profile, runner, require_workspace=False)
@@ -151,6 +172,7 @@ class WorkspaceTools(ToolBase):
151
172
 
152
173
  def runner(_, context):
153
174
  workspace = self._fetch_workspace_with_fallback(context, ws_id=ws_id)
175
+ workspace, warnings = _strip_workspace_detail_warning(workspace)
154
176
  workspace_name = str(workspace.get("workspaceName") or workspace.get("wsName") or workspace.get("remark") or "").strip() or None
155
177
  selected = self.sessions.select_workspace(profile, ws_id=ws_id, ws_name=workspace_name)
156
178
  system_version = self._workspace_system_version(workspace)
@@ -171,6 +193,11 @@ class WorkspaceTools(ToolBase):
171
193
  "ws_id": selected.selected_ws_id,
172
194
  "workspace_name": selected.selected_ws_name,
173
195
  },
196
+ "warnings": warnings,
197
+ "verification": {
198
+ "workspace_detail_verified": not warnings,
199
+ "workspace_list_fallback_used": bool(warnings),
200
+ },
174
201
  }
175
202
 
176
203
  return self._run(profile, runner, require_workspace=False)
@@ -212,7 +239,15 @@ class WorkspaceTools(ToolBase):
212
239
  *,
213
240
  ws_id: int,
214
241
  ) -> dict[str, Any]:
215
- workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
242
+ try:
243
+ workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
244
+ except QingflowApiError as error:
245
+ if not _is_optional_workspace_detail_error(error):
246
+ raise
247
+ fallback = self._fetch_workspace_from_list(context, ws_id=ws_id)
248
+ if isinstance(fallback, dict):
249
+ return _with_workspace_detail_warning(fallback, error, ws_id=ws_id)
250
+ raise
216
251
  if not isinstance(workspace, dict):
217
252
  raise_tool_error(QingflowApiError(category="workspace", message=f"Workspace {ws_id} is not accessible"))
218
253
  if self._workspace_needs_list_fallback(workspace):
@@ -264,3 +299,36 @@ class WorkspaceTools(ToolBase):
264
299
  return None
265
300
  normalized = str(value).strip()
266
301
  return normalized or None
302
+
303
+
304
+ def _is_optional_workspace_detail_error(error: QingflowApiError) -> bool:
305
+ if is_auth_like_error(error):
306
+ return False
307
+ backend_code = backend_code_int(error)
308
+ return backend_code in {40002, 40027, 404, 59004} or error.http_status == 404
309
+
310
+
311
+ def _with_workspace_detail_warning(workspace: dict[str, Any], error: QingflowApiError, *, ws_id: int) -> dict[str, Any]:
312
+ enriched = dict(workspace)
313
+ enriched[_WORKSPACE_DETAIL_WARNING_KEY] = _workspace_detail_fallback_warning(error, ws_id=ws_id)
314
+ return enriched
315
+
316
+
317
+ def _strip_workspace_detail_warning(workspace: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]:
318
+ public_workspace = dict(workspace)
319
+ warning = public_workspace.pop(_WORKSPACE_DETAIL_WARNING_KEY, None)
320
+ warnings = [warning] if isinstance(warning, dict) else []
321
+ return public_workspace, warnings
322
+
323
+
324
+ def _workspace_detail_fallback_warning(error: QingflowApiError, *, ws_id: int) -> dict[str, Any]:
325
+ return {
326
+ "code": "WORKSPACE_DETAIL_UNAVAILABLE",
327
+ "message": (
328
+ f"workspace detail route for ws_id {ws_id} was unavailable; "
329
+ "the tool used the visible workspace list as fallback."
330
+ ),
331
+ "backend_code": error.backend_code,
332
+ "http_status": error.http_status,
333
+ "request_id": error.request_id,
334
+ }