@josephyan/qingflow-cli 1.1.4 → 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 +282 -102
  93. package/src/qingflow_mcp/builder_facade/service.py +4166 -929
  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
@@ -3,7 +3,7 @@ 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
8
8
  from .base import ToolBase, tool_cn_name
9
9
 
@@ -85,8 +85,26 @@ class NavigationTools(ToolBase):
85
85
  params: JSONObject = {"pageNum": page_num, "pageSize": page_size}
86
86
  if query_key:
87
87
  params["queryCondition"] = query_key
88
- result = self.backend.request("GET", context, "/navigation/page", params=params)
89
- return {"profile": profile, "ws_id": session_profile.selected_ws_id, "page": result}
88
+ fallback_error: QingflowApiError | None = None
89
+ try:
90
+ result = self.backend.request("GET", context, "/navigation/page", params=params)
91
+ response = {"profile": profile, "ws_id": session_profile.selected_ws_id, "status": "draft", "page": result}
92
+ except QingflowApiError as exc:
93
+ if not _is_optional_draft_navigation_read_error(exc):
94
+ raise
95
+ fallback_error = exc
96
+ result = self.backend.request("GET", context, "/navigation", params={"pageNum": page_num, "pageSize": page_size})
97
+ response = {
98
+ "profile": profile,
99
+ "ws_id": session_profile.selected_ws_id,
100
+ "status": "published",
101
+ "requested_status": "draft",
102
+ "page": result,
103
+ }
104
+ if fallback_error is not None:
105
+ response["warnings"] = [_navigation_fallback_warning("NAVIGATION_DRAFT_PAGE_UNAVAILABLE", fallback_error)]
106
+ response["verification"] = {"draft_readable": False, "published_fallback_used": True}
107
+ return response
90
108
 
91
109
  return self._run(profile, runner)
92
110
 
@@ -97,8 +115,26 @@ class NavigationTools(ToolBase):
97
115
  params: JSONObject = {}
98
116
  if query_key:
99
117
  params["queryCondition"] = query_key
100
- result = self.backend.request("GET", context, "/navigation/all", params=params)
101
- return {"profile": profile, "ws_id": session_profile.selected_ws_id, "items": result}
118
+ fallback_error: QingflowApiError | None = None
119
+ try:
120
+ result = self.backend.request("GET", context, "/navigation/all", params=params)
121
+ response = {"profile": profile, "ws_id": session_profile.selected_ws_id, "status": "draft", "items": result}
122
+ except QingflowApiError as exc:
123
+ if not _is_optional_draft_navigation_read_error(exc):
124
+ raise
125
+ fallback_error = exc
126
+ result = self.backend.request("GET", context, "/navigation", params={"pageNum": 1, "pageSize": 1000})
127
+ response = {
128
+ "profile": profile,
129
+ "ws_id": session_profile.selected_ws_id,
130
+ "status": "published",
131
+ "requested_status": "draft",
132
+ "items": result,
133
+ }
134
+ if fallback_error is not None:
135
+ response["warnings"] = [_navigation_fallback_warning("NAVIGATION_DRAFT_ALL_UNAVAILABLE", fallback_error)]
136
+ response["verification"] = {"draft_readable": False, "published_fallback_used": True}
137
+ return response
102
138
 
103
139
  return self._run(profile, runner)
104
140
 
@@ -108,13 +144,39 @@ class NavigationTools(ToolBase):
108
144
  self._require_navigation_item_id(navigation_item_id)
109
145
 
110
146
  def runner(session_profile, context):
111
- result = self.backend.request(
112
- "GET",
113
- context,
114
- f"/navigation/detail/{navigation_item_id}",
115
- params={"status": status},
116
- )
117
- return {"profile": profile, "ws_id": session_profile.selected_ws_id, "navigation_item_id": navigation_item_id, "result": result}
147
+ requested_status = str(status or "draft")
148
+ fallback_error: QingflowApiError | None = None
149
+ try:
150
+ result = self.backend.request(
151
+ "GET",
152
+ context,
153
+ f"/navigation/detail/{navigation_item_id}",
154
+ params={"status": requested_status},
155
+ )
156
+ effective_status = requested_status
157
+ except QingflowApiError as exc:
158
+ if requested_status != "draft" or not _is_optional_draft_navigation_read_error(exc):
159
+ raise
160
+ fallback_error = exc
161
+ effective_status = "published"
162
+ result = self.backend.request(
163
+ "GET",
164
+ context,
165
+ f"/navigation/detail/{navigation_item_id}",
166
+ params={"status": effective_status},
167
+ )
168
+ response = {
169
+ "profile": profile,
170
+ "ws_id": session_profile.selected_ws_id,
171
+ "navigation_item_id": navigation_item_id,
172
+ "status": effective_status,
173
+ "requested_status": requested_status,
174
+ "result": result,
175
+ }
176
+ if fallback_error is not None:
177
+ response["warnings"] = [_navigation_fallback_warning("NAVIGATION_DRAFT_DETAIL_UNAVAILABLE", fallback_error)]
178
+ response["verification"] = {"draft_readable": False, "published_fallback_used": True}
179
+ return response
118
180
 
119
181
  return self._run(profile, runner)
120
182
 
@@ -208,3 +270,20 @@ class NavigationTools(ToolBase):
208
270
  """执行内部辅助逻辑。"""
209
271
  if navigation_item_id <= 0:
210
272
  raise_tool_error(QingflowApiError.config_error("navigation_item_id must be positive"))
273
+
274
+
275
+ def _is_optional_draft_navigation_read_error(error: QingflowApiError) -> bool:
276
+ if is_auth_like_error(error):
277
+ return False
278
+ backend_code = backend_code_int(error)
279
+ return backend_code in {40002, 40027, 404} or error.http_status == 404
280
+
281
+
282
+ def _navigation_fallback_warning(code: str, error: QingflowApiError) -> JSONObject:
283
+ return {
284
+ "code": code,
285
+ "message": "draft navigation data is unavailable; returned published navigation data instead",
286
+ "backend_code": error.backend_code,
287
+ "http_status": error.http_status,
288
+ "request_id": error.request_id,
289
+ }
@@ -5,7 +5,7 @@ from copy import deepcopy
5
5
  from mcp.server.fastmcp import FastMCP
6
6
 
7
7
  from ..config import DEFAULT_PROFILE
8
- from ..errors import QingflowApiError, raise_tool_error
8
+ from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
9
9
  from ..json_types import JSONObject
10
10
  from ..solution.compiler.icon_utils import workspace_icon_config
11
11
  from .base import ToolBase, tool_cn_name
@@ -76,8 +76,44 @@ class PackageTools(ToolBase):
76
76
  self._require_tag_id(tag_id)
77
77
 
78
78
  def runner(session_profile, context):
79
- result = self.backend.request("GET", context, f"/tag/{tag_id}")
80
- base_info = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
79
+ warnings: list[JSONObject] = []
80
+ verification: JSONObject = {"base_info_verified": True, "fallback_visible_package_used": False}
81
+ try:
82
+ base_info = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
83
+ except QingflowApiError as exc:
84
+ if not _is_optional_package_metadata_error(exc):
85
+ raise
86
+ base_info = self._resolve_visible_package(context, tag_id)
87
+ if base_info is None:
88
+ raise
89
+ warnings.append(
90
+ {
91
+ "code": "PACKAGE_BASE_INFO_UNAVAILABLE",
92
+ "message": "package_get used the current user's visible package list because package baseInfo is unavailable in this permission context.",
93
+ "backend_code": exc.backend_code,
94
+ "http_status": exc.http_status,
95
+ "request_id": exc.request_id,
96
+ }
97
+ )
98
+ verification = {"base_info_verified": False, "fallback_visible_package_used": True}
99
+ result: JSONObject = base_info if isinstance(base_info, dict) else {}
100
+ if verification["base_info_verified"]:
101
+ try:
102
+ detail = self.backend.request("GET", context, f"/tag/{tag_id}")
103
+ if isinstance(detail, dict):
104
+ result = detail
105
+ except QingflowApiError as exc:
106
+ if not _is_optional_package_metadata_error(exc):
107
+ raise
108
+ warnings.append(
109
+ {
110
+ "code": "PACKAGE_DETAIL_READ_DEGRADED",
111
+ "message": "package_get used package baseInfo because the detail endpoint requires package edit/add-app permission.",
112
+ "backend_code": exc.backend_code,
113
+ "http_status": exc.http_status,
114
+ "request_id": exc.request_id,
115
+ }
116
+ )
81
117
  compact = self._compact_package(
82
118
  result if isinstance(result, dict) else {},
83
119
  base_info=base_info if isinstance(base_info, dict) else None,
@@ -88,7 +124,11 @@ class PackageTools(ToolBase):
88
124
  "tag_id": tag_id,
89
125
  "result": result if include_raw else compact,
90
126
  "compact": not include_raw,
127
+ "detail_degraded": bool(warnings),
128
+ "verification": verification,
91
129
  }
130
+ if warnings:
131
+ response["warnings"] = warnings
92
132
  if include_raw:
93
133
  response["summary"] = compact
94
134
  return response
@@ -101,7 +141,27 @@ class PackageTools(ToolBase):
101
141
  self._require_readable_tag_id(tag_id)
102
142
 
103
143
  def runner(session_profile, context):
104
- result = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
144
+ warnings: list[JSONObject] = []
145
+ verification: JSONObject = {"base_info_verified": True, "fallback_visible_package_used": False}
146
+ try:
147
+ result = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
148
+ except QingflowApiError as exc:
149
+ if not _is_optional_package_metadata_error(exc):
150
+ raise
151
+ visible_package = self._resolve_visible_package(context, tag_id)
152
+ if visible_package is None:
153
+ raise
154
+ result = visible_package
155
+ warnings.append(
156
+ {
157
+ "code": "PACKAGE_BASE_INFO_UNAVAILABLE",
158
+ "message": "package_get_base used the current user's visible package list because package baseInfo is unavailable in this permission context.",
159
+ "backend_code": exc.backend_code,
160
+ "http_status": exc.http_status,
161
+ "request_id": exc.request_id,
162
+ }
163
+ )
164
+ verification = {"base_info_verified": False, "fallback_visible_package_used": True}
105
165
  compact = self._compact_package(result if isinstance(result, dict) else {})
106
166
  response = {
107
167
  "profile": profile,
@@ -109,7 +169,10 @@ class PackageTools(ToolBase):
109
169
  "tag_id": tag_id,
110
170
  "result": result if include_raw else compact,
111
171
  "compact": not include_raw,
172
+ "verification": verification,
112
173
  }
174
+ if warnings:
175
+ response["warnings"] = warnings
113
176
  if include_raw:
114
177
  response["summary"] = compact
115
178
  return response
@@ -134,15 +197,44 @@ class PackageTools(ToolBase):
134
197
  body = self._require_dict(payload)
135
198
 
136
199
  def runner(session_profile, context):
137
- current = self.backend.request("GET", context, f"/tag/{tag_id}")
200
+ warnings: list[JSONObject] = []
201
+ try:
202
+ current = self.backend.request("GET", context, f"/tag/{tag_id}")
203
+ except QingflowApiError as exc:
204
+ if not _is_optional_package_metadata_error(exc):
205
+ raise
206
+ try:
207
+ current = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
208
+ fallback_source = "baseInfo"
209
+ except QingflowApiError as base_exc:
210
+ if not _is_optional_package_metadata_error(base_exc):
211
+ raise
212
+ visible_package = self._resolve_visible_package(context, tag_id)
213
+ if visible_package is None:
214
+ raise base_exc
215
+ current = visible_package
216
+ fallback_source = "visible_package_list"
217
+ warnings.append(
218
+ {
219
+ "code": "PACKAGE_DETAIL_READ_DEGRADED",
220
+ "message": "package_update used package baseInfo or visible package metadata because package detail is unavailable in this permission context.",
221
+ "source": fallback_source,
222
+ "backend_code": exc.backend_code,
223
+ "http_status": exc.http_status,
224
+ "request_id": exc.request_id,
225
+ }
226
+ )
138
227
  result = self.backend.request(
139
228
  "PUT",
140
229
  context,
141
230
  f"/tag/{tag_id}",
142
231
  json_body=self._normalize_package_payload(body, existing=current),
143
232
  )
233
+ response = {"profile": profile, "ws_id": session_profile.selected_ws_id, "tag_id": tag_id, "result": result}
234
+ if warnings:
235
+ response["warnings"] = warnings
144
236
  return self._attach_human_review_notice(
145
- {"profile": profile, "ws_id": session_profile.selected_ws_id, "tag_id": tag_id, "result": result},
237
+ response,
146
238
  operation="update",
147
239
  target="app package settings",
148
240
  )
@@ -251,6 +343,19 @@ class PackageTools(ToolBase):
251
343
  if tag_id < 0:
252
344
  raise_tool_error(QingflowApiError.config_error("tag_id must be non-negative"))
253
345
 
346
+ def _resolve_visible_package(self, context, tag_id: int) -> JSONObject | None: # type: ignore[no-untyped-def]
347
+ """Resolve package metadata from the current user's visible package list."""
348
+ payload = self.backend.request("GET", context, "/tag", params={"trialStatus": "all"})
349
+ items, _source_shape = self._extract_package_items(payload)
350
+ for item in items:
351
+ try:
352
+ candidate = int(item.get("tagId")) if item.get("tagId") is not None else None
353
+ except (TypeError, ValueError):
354
+ candidate = None
355
+ if candidate == tag_id:
356
+ return dict(item)
357
+ return None
358
+
254
359
  def _normalize_package_payload(self, payload: JSONObject, existing: JSONObject | None = None) -> JSONObject:
255
360
  """执行内部辅助逻辑。"""
256
361
  data = deepcopy(existing) if isinstance(existing, dict) else {}
@@ -338,3 +443,10 @@ def _default_package_auth() -> JSONObject:
338
443
  "contactAuth": {"type": "WORKSPACE_ALL", "authMembers": deepcopy(members)},
339
444
  "externalMemberAuth": {"type": "NOT", "authMembers": deepcopy(members)},
340
445
  }
446
+
447
+
448
+ def _is_optional_package_metadata_error(error: QingflowApiError) -> bool:
449
+ if is_auth_like_error(error):
450
+ return False
451
+ backend_code = backend_code_int(error)
452
+ return backend_code in {40002, 40027, 404} or error.http_status == 404
@@ -3,7 +3,7 @@ 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
8
8
  from .base import ToolBase, tool_cn_name
9
9
 
@@ -89,8 +89,37 @@ class PortalTools(ToolBase):
89
89
  self._require_dash_key(dash_key)
90
90
 
91
91
  def runner(session_profile, context):
92
- result = self.backend.request("GET", context, f"/dash/{dash_key}/baseInfo")
93
- return {"profile": profile, "ws_id": session_profile.selected_ws_id, "dash_key": dash_key, "result": result}
92
+ warnings: list[JSONObject] = []
93
+ verification: JSONObject = {"base_info_verified": True, "fallback_detail_used": False}
94
+ try:
95
+ result = self.backend.request("GET", context, f"/dash/{dash_key}/baseInfo")
96
+ except QingflowApiError as error:
97
+ if not _is_optional_portal_base_info_error(error):
98
+ raise
99
+ result = self.backend.request("GET", context, f"/dash/{dash_key}", params={"beingDraft": False})
100
+ verification["base_info_verified"] = False
101
+ verification["fallback_detail_used"] = True
102
+ warning: JSONObject = {
103
+ "code": "PORTAL_BASE_INFO_UNAVAILABLE",
104
+ "message": "portal_get_base_info used portal detail because portal baseInfo is unavailable in this permission context.",
105
+ }
106
+ if error.backend_code is not None:
107
+ warning["backend_code"] = error.backend_code
108
+ if error.http_status is not None:
109
+ warning["http_status"] = error.http_status
110
+ if error.request_id is not None:
111
+ warning["request_id"] = error.request_id
112
+ if error.details:
113
+ warning["details"] = error.details
114
+ warnings.append(warning)
115
+ return {
116
+ "profile": profile,
117
+ "ws_id": session_profile.selected_ws_id,
118
+ "dash_key": dash_key,
119
+ "result": result,
120
+ "warnings": warnings,
121
+ "verification": verification,
122
+ }
94
123
 
95
124
  return self._run(profile, runner)
96
125
 
@@ -156,3 +185,10 @@ class PortalTools(ToolBase):
156
185
  """执行内部辅助逻辑。"""
157
186
  if not dash_key:
158
187
  raise_tool_error(QingflowApiError.config_error("dash_key is required"))
188
+
189
+
190
+ def _is_optional_portal_base_info_error(error: QingflowApiError) -> bool:
191
+ if is_auth_like_error(error):
192
+ return False
193
+ backend_code = backend_code_int(error)
194
+ return backend_code in {40002, 40027, 404} or error.http_status == 404
@@ -7,7 +7,7 @@ from mcp.server.fastmcp import FastMCP
7
7
 
8
8
  from ..backend_client import BackendRequestContext
9
9
  from ..config import DEFAULT_PROFILE, normalize_base_url
10
- from ..errors import QingflowApiError, raise_tool_error
10
+ from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
11
11
  from ..json_types import JSONObject, JSONValue
12
12
  from .base import ToolBase, tool_cn_name
13
13
 
@@ -20,13 +20,27 @@ def _qingbi_base_url(base_url: str) -> str:
20
20
 
21
21
 
22
22
  def _should_retry_qflow_base(error: QingflowApiError) -> bool:
23
- return int(getattr(error, "backend_code", 0) or 0) == 81007
23
+ if is_auth_like_error(error):
24
+ return False
25
+ backend_code = backend_code_int(error)
26
+ http_status = getattr(error, "http_status", None)
27
+ return backend_code in {40002, 40027, 404, 81007} or http_status == 404
24
28
 
25
29
 
26
30
  def _should_retry_asos_data(error: QingflowApiError) -> bool:
27
- backend_code = int(getattr(error, "backend_code", 0) or 0)
31
+ if is_auth_like_error(error):
32
+ return False
33
+ backend_code = backend_code_int(error)
28
34
  http_status = getattr(error, "http_status", None)
29
- return backend_code in {44011, 81007} or http_status == 404
35
+ return backend_code in {40002, 40027, 404, 44011, 81007} or http_status == 404
36
+
37
+
38
+ def _should_fallback_config_from_data(error: QingflowApiError) -> bool:
39
+ if is_auth_like_error(error):
40
+ return False
41
+ backend_code = backend_code_int(error)
42
+ http_status = getattr(error, "http_status", None)
43
+ return backend_code in {40002, 40027, 404, 81007} or http_status == 404
30
44
 
31
45
 
32
46
  def _coerce_tool_error(error: RuntimeError | QingflowApiError) -> QingflowApiError | None:
@@ -177,13 +191,37 @@ class QingbiReportTools(ToolBase):
177
191
  def qingbi_report_get_base(self, *, profile: str, chart_id: str) -> JSONObject:
178
192
  """执行工具方法逻辑。"""
179
193
  self._require_chart_id(chart_id)
194
+ source_error: QingflowApiError | None = None
180
195
  try:
181
- return self._request(profile, "GET", f"/qingbi/charts/baseinfo/{chart_id}", chart_id=chart_id)
196
+ return self._request(profile, "GET", f"/qingbi/charts/qflow/baseinfo/{chart_id}", chart_id=chart_id)
182
197
  except (QingflowApiError, RuntimeError) as raw_error:
183
198
  error = _coerce_tool_error(raw_error)
184
199
  if error is None or not _should_retry_qflow_base(error):
185
200
  raise
186
- return self._request(profile, "GET", f"/qingbi/charts/qflow/baseinfo/{chart_id}", chart_id=chart_id)
201
+ source_error = error
202
+ try:
203
+ fallback = self._request(profile, "GET", f"/qingbi/charts/baseinfo/{chart_id}", chart_id=chart_id)
204
+ except (QingflowApiError, RuntimeError):
205
+ raise
206
+ if not _has_chart_base_identity(fallback.get("result"), chart_id=chart_id):
207
+ raise source_error
208
+ fallback.setdefault(
209
+ "warnings",
210
+ [
211
+ {
212
+ "code": "CHART_BASE_INFO_FALLBACK_FROM_LEGACY",
213
+ "message": "qingbi_report_get_base used the legacy chart baseInfo route because qflow baseInfo was unavailable in this permission context.",
214
+ "backend_code": source_error.backend_code,
215
+ "http_status": source_error.http_status,
216
+ "request_id": source_error.request_id,
217
+ }
218
+ ],
219
+ )
220
+ fallback.setdefault(
221
+ "verification",
222
+ {"qflow_base_route_loaded": False, "legacy_base_route_loaded": True},
223
+ )
224
+ return fallback
187
225
 
188
226
  @tool_cn_name("更新报表基础信息")
189
227
  def qingbi_report_update_base(self, *, profile: str, chart_id: str, payload: JSONObject) -> JSONObject:
@@ -196,7 +234,45 @@ class QingbiReportTools(ToolBase):
196
234
  def qingbi_report_get_config(self, *, profile: str, chart_id: str) -> JSONObject:
197
235
  """执行工具方法逻辑。"""
198
236
  self._require_chart_id(chart_id)
199
- return self._request(profile, "GET", f"/qingbi/charts/{chart_id}/configs", chart_id=chart_id)
237
+ original_error: QingflowApiError | RuntimeError | None = None
238
+ try:
239
+ return self._request(profile, "GET", f"/qingbi/charts/{chart_id}/configs", chart_id=chart_id)
240
+ except (QingflowApiError, RuntimeError) as raw_error:
241
+ original_error = raw_error
242
+ error = _coerce_tool_error(raw_error)
243
+ if error is None or not _should_fallback_config_from_data(error):
244
+ raise
245
+ source_error = error
246
+ try:
247
+ data_payload = self.qingbi_report_get_data(profile=profile, chart_id=chart_id, payload={})
248
+ except (QingflowApiError, RuntimeError):
249
+ if original_error is not None:
250
+ raise original_error
251
+ raise
252
+ data_result = data_payload.get("result") if isinstance(data_payload, dict) else None
253
+ if isinstance(data_result, dict) and isinstance(data_result.get("config"), dict) and data_result["config"]:
254
+ return {
255
+ "profile": data_payload.get("profile", profile),
256
+ "ws_id": data_payload.get("ws_id"),
257
+ "chart_id": chart_id,
258
+ "result": data_result["config"],
259
+ "warnings": [
260
+ {
261
+ "code": "CHART_CONFIG_FALLBACK_FROM_DATA",
262
+ "message": "qingbi_report_get_config used config embedded in qflow chart data because the chart config endpoint was unavailable in this permission context.",
263
+ "backend_code": source_error.backend_code,
264
+ "http_status": source_error.http_status,
265
+ "request_id": source_error.request_id,
266
+ }
267
+ ],
268
+ "verification": {
269
+ "config_endpoint_loaded": False,
270
+ "data_config_loaded": True,
271
+ },
272
+ }
273
+ if original_error is not None:
274
+ raise original_error
275
+ raise_tool_error(QingflowApiError.config_error("chart config fallback did not return config"))
200
276
 
201
277
  @tool_cn_name("更新报表配置")
202
278
  def qingbi_report_update_config(self, *, profile: str, chart_id: str, payload: JSONObject) -> JSONObject:
@@ -251,6 +327,16 @@ class QingbiReportTools(ToolBase):
251
327
  error = _coerce_tool_error(raw_error)
252
328
  if error is None or not _should_retry_asos_data(error):
253
329
  raise
330
+ warning: JSONObject = {
331
+ "code": "CHART_DATA_FALLBACK_FROM_ASOS",
332
+ "message": "qingbi_report_get_data used the asos data route because the qflow chart data route was unavailable in this permission context.",
333
+ }
334
+ if error.backend_code is not None:
335
+ warning["backend_code"] = error.backend_code
336
+ if error.http_status is not None:
337
+ warning["http_status"] = error.http_status
338
+ if error.request_id:
339
+ warning["request_id"] = error.request_id
254
340
  return self._request(
255
341
  profile,
256
342
  "POST",
@@ -258,6 +344,11 @@ class QingbiReportTools(ToolBase):
258
344
  chart_id=chart_id,
259
345
  params=params,
260
346
  json_body=payload or {},
347
+ warnings=[warning],
348
+ verification={
349
+ "qflow_data_route_loaded": False,
350
+ "asos_data_route_loaded": True,
351
+ },
261
352
  )
262
353
 
263
354
  @tool_cn_name("删除报表")
@@ -372,3 +463,21 @@ def _extract_sorted_chart_items(result: JSONValue) -> list[JSONObject]:
372
463
  if isinstance(result, list):
373
464
  return result
374
465
  return []
466
+
467
+
468
+ def _has_chart_base_identity(result: JSONValue, *, chart_id: str) -> bool:
469
+ if not isinstance(result, dict):
470
+ return False
471
+ expected = str(chart_id or "").strip()
472
+ id_candidates = (
473
+ result.get("chartId"),
474
+ result.get("biChartId"),
475
+ result.get("id"),
476
+ result.get("chart_id"),
477
+ )
478
+ if expected and any(str(value or "").strip() == expected for value in id_candidates):
479
+ return True
480
+ return any(
481
+ str(result.get(key) or "").strip()
482
+ for key in ("chartName", "chart_name", "name", "title")
483
+ )