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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +5 -6
  2. package/docs/local-agent-install.md +6 -57
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
  5. package/npm/lib/runtime.mjs +101 -21
  6. package/npm/scripts/postinstall.mjs +10 -1
  7. package/package.json +2 -3
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +42 -104
  10. package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
  11. package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
  12. package/skills/qingflow-app-builder/references/create-app.md +11 -30
  13. package/skills/qingflow-app-builder/references/environments.md +1 -1
  14. package/skills/qingflow-app-builder/references/gotchas.md +9 -31
  15. package/skills/qingflow-app-builder/references/match-rules.md +0 -15
  16. package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
  17. package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
  18. package/skills/qingflow-app-builder/references/update-flow.md +38 -22
  19. package/skills/qingflow-app-builder/references/update-schema.md +0 -14
  20. package/skills/qingflow-app-builder/references/update-views.md +14 -83
  21. package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
  22. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
  23. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
  24. package/skills/qingflow-mcp-setup/SKILL.md +111 -0
  25. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  26. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  27. package/skills/qingflow-mcp-setup/references/environments.md +61 -0
  28. package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
  29. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  30. package/skills/qingflow-workflow-builder/SKILL.md +96 -0
  31. package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
  32. package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
  33. package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
  34. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
  35. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
  36. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
  37. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
  38. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
  39. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
  40. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
  41. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
  42. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
  43. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
  44. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
  45. package/src/qingflow_mcp/__init__.py +1 -1
  46. package/src/qingflow_mcp/__main__.py +2 -6
  47. package/src/qingflow_mcp/builder_facade/models.py +108 -485
  48. package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
  49. package/src/qingflow_mcp/cli/commands/builder.py +364 -322
  50. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  51. package/src/qingflow_mcp/cli/commands/common.py +3 -12
  52. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  53. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  54. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  55. package/src/qingflow_mcp/cli/commands/record.py +27 -101
  56. package/src/qingflow_mcp/cli/commands/task.py +47 -28
  57. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  58. package/src/qingflow_mcp/cli/context.py +3 -0
  59. package/src/qingflow_mcp/cli/formatters.py +16 -784
  60. package/src/qingflow_mcp/cli/main.py +33 -117
  61. package/src/qingflow_mcp/errors.py +2 -43
  62. package/src/qingflow_mcp/public_surface.py +17 -26
  63. package/src/qingflow_mcp/response_trim.py +17 -81
  64. package/src/qingflow_mcp/server.py +12 -14
  65. package/src/qingflow_mcp/server_app_builder.py +36 -79
  66. package/src/qingflow_mcp/server_app_user.py +16 -22
  67. package/src/qingflow_mcp/session_store.py +7 -11
  68. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  69. package/src/qingflow_mcp/solution/executor.py +18 -245
  70. package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
  71. package/src/qingflow_mcp/tools/app_tools.py +43 -184
  72. package/src/qingflow_mcp/tools/approval_tools.py +35 -197
  73. package/src/qingflow_mcp/tools/auth_tools.py +16 -92
  74. package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
  75. package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
  76. package/src/qingflow_mcp/tools/directory_tools.py +72 -236
  77. package/src/qingflow_mcp/tools/export_tools.py +34 -244
  78. package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
  79. package/src/qingflow_mcp/tools/file_tools.py +3 -9
  80. package/src/qingflow_mcp/tools/import_tools.py +49 -336
  81. package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
  82. package/src/qingflow_mcp/tools/package_tools.py +6 -118
  83. package/src/qingflow_mcp/tools/portal_tools.py +3 -39
  84. package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
  85. package/src/qingflow_mcp/tools/record_tools.py +360 -1145
  86. package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
  87. package/src/qingflow_mcp/tools/role_tools.py +9 -80
  88. package/src/qingflow_mcp/tools/solution_tools.py +45 -59
  89. package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
  90. package/src/qingflow_mcp/tools/task_tools.py +29 -113
  91. package/src/qingflow_mcp/tools/view_tools.py +3 -106
  92. package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
  93. package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
  94. package/npm/bin/qingflow-skills.mjs +0 -5
  95. package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
  96. package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
  97. package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
  98. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -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, backend_code_int, is_auth_like_error, raise_tool_error
6
+ from ..errors import QingflowApiError, raise_tool_error
7
7
  from ..json_types import JSONObject
8
8
  from .base import ToolBase, tool_cn_name
9
9
 
@@ -85,26 +85,8 @@ 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
- 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
88
+ result = self.backend.request("GET", context, "/navigation/page", params=params)
89
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "page": result}
108
90
 
109
91
  return self._run(profile, runner)
110
92
 
@@ -115,26 +97,8 @@ class NavigationTools(ToolBase):
115
97
  params: JSONObject = {}
116
98
  if query_key:
117
99
  params["queryCondition"] = query_key
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
100
+ result = self.backend.request("GET", context, "/navigation/all", params=params)
101
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "items": result}
138
102
 
139
103
  return self._run(profile, runner)
140
104
 
@@ -144,39 +108,13 @@ class NavigationTools(ToolBase):
144
108
  self._require_navigation_item_id(navigation_item_id)
145
109
 
146
110
  def runner(session_profile, context):
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
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}
180
118
 
181
119
  return self._run(profile, runner)
182
120
 
@@ -270,20 +208,3 @@ class NavigationTools(ToolBase):
270
208
  """执行内部辅助逻辑。"""
271
209
  if navigation_item_id <= 0:
272
210
  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, backend_code_int, is_auth_like_error, raise_tool_error
8
+ from ..errors import QingflowApiError, 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,44 +76,8 @@ class PackageTools(ToolBase):
76
76
  self._require_tag_id(tag_id)
77
77
 
78
78
  def runner(session_profile, context):
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
- )
79
+ result = self.backend.request("GET", context, f"/tag/{tag_id}")
80
+ base_info = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
117
81
  compact = self._compact_package(
118
82
  result if isinstance(result, dict) else {},
119
83
  base_info=base_info if isinstance(base_info, dict) else None,
@@ -124,11 +88,7 @@ class PackageTools(ToolBase):
124
88
  "tag_id": tag_id,
125
89
  "result": result if include_raw else compact,
126
90
  "compact": not include_raw,
127
- "detail_degraded": bool(warnings),
128
- "verification": verification,
129
91
  }
130
- if warnings:
131
- response["warnings"] = warnings
132
92
  if include_raw:
133
93
  response["summary"] = compact
134
94
  return response
@@ -141,27 +101,7 @@ class PackageTools(ToolBase):
141
101
  self._require_readable_tag_id(tag_id)
142
102
 
143
103
  def runner(session_profile, context):
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}
104
+ result = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
165
105
  compact = self._compact_package(result if isinstance(result, dict) else {})
166
106
  response = {
167
107
  "profile": profile,
@@ -169,10 +109,7 @@ class PackageTools(ToolBase):
169
109
  "tag_id": tag_id,
170
110
  "result": result if include_raw else compact,
171
111
  "compact": not include_raw,
172
- "verification": verification,
173
112
  }
174
- if warnings:
175
- response["warnings"] = warnings
176
113
  if include_raw:
177
114
  response["summary"] = compact
178
115
  return response
@@ -197,44 +134,15 @@ class PackageTools(ToolBase):
197
134
  body = self._require_dict(payload)
198
135
 
199
136
  def runner(session_profile, context):
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
- )
137
+ current = self.backend.request("GET", context, f"/tag/{tag_id}")
227
138
  result = self.backend.request(
228
139
  "PUT",
229
140
  context,
230
141
  f"/tag/{tag_id}",
231
142
  json_body=self._normalize_package_payload(body, existing=current),
232
143
  )
233
- response = {"profile": profile, "ws_id": session_profile.selected_ws_id, "tag_id": tag_id, "result": result}
234
- if warnings:
235
- response["warnings"] = warnings
236
144
  return self._attach_human_review_notice(
237
- response,
145
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "tag_id": tag_id, "result": result},
238
146
  operation="update",
239
147
  target="app package settings",
240
148
  )
@@ -343,19 +251,6 @@ class PackageTools(ToolBase):
343
251
  if tag_id < 0:
344
252
  raise_tool_error(QingflowApiError.config_error("tag_id must be non-negative"))
345
253
 
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
-
359
254
  def _normalize_package_payload(self, payload: JSONObject, existing: JSONObject | None = None) -> JSONObject:
360
255
  """执行内部辅助逻辑。"""
361
256
  data = deepcopy(existing) if isinstance(existing, dict) else {}
@@ -443,10 +338,3 @@ def _default_package_auth() -> JSONObject:
443
338
  "contactAuth": {"type": "WORKSPACE_ALL", "authMembers": deepcopy(members)},
444
339
  "externalMemberAuth": {"type": "NOT", "authMembers": deepcopy(members)},
445
340
  }
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, backend_code_int, is_auth_like_error, raise_tool_error
6
+ from ..errors import QingflowApiError, raise_tool_error
7
7
  from ..json_types import JSONObject
8
8
  from .base import ToolBase, tool_cn_name
9
9
 
@@ -89,37 +89,8 @@ class PortalTools(ToolBase):
89
89
  self._require_dash_key(dash_key)
90
90
 
91
91
  def runner(session_profile, context):
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
- }
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}
123
94
 
124
95
  return self._run(profile, runner)
125
96
 
@@ -185,10 +156,3 @@ class PortalTools(ToolBase):
185
156
  """执行内部辅助逻辑。"""
186
157
  if not dash_key:
187
158
  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, backend_code_int, is_auth_like_error, raise_tool_error
10
+ from ..errors import QingflowApiError, raise_tool_error
11
11
  from ..json_types import JSONObject, JSONValue
12
12
  from .base import ToolBase, tool_cn_name
13
13
 
@@ -20,27 +20,13 @@ def _qingbi_base_url(base_url: str) -> str:
20
20
 
21
21
 
22
22
  def _should_retry_qflow_base(error: QingflowApiError) -> bool:
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
23
+ return int(getattr(error, "backend_code", 0) or 0) == 81007
28
24
 
29
25
 
30
26
  def _should_retry_asos_data(error: QingflowApiError) -> bool:
31
- if is_auth_like_error(error):
32
- return False
33
- backend_code = backend_code_int(error)
27
+ backend_code = int(getattr(error, "backend_code", 0) or 0)
34
28
  http_status = getattr(error, "http_status", None)
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
29
+ return backend_code in {44011, 81007} or http_status == 404
44
30
 
45
31
 
46
32
  def _coerce_tool_error(error: RuntimeError | QingflowApiError) -> QingflowApiError | None:
@@ -191,37 +177,13 @@ class QingbiReportTools(ToolBase):
191
177
  def qingbi_report_get_base(self, *, profile: str, chart_id: str) -> JSONObject:
192
178
  """执行工具方法逻辑。"""
193
179
  self._require_chart_id(chart_id)
194
- source_error: QingflowApiError | None = None
195
180
  try:
196
- return self._request(profile, "GET", f"/qingbi/charts/qflow/baseinfo/{chart_id}", chart_id=chart_id)
181
+ return self._request(profile, "GET", f"/qingbi/charts/baseinfo/{chart_id}", chart_id=chart_id)
197
182
  except (QingflowApiError, RuntimeError) as raw_error:
198
183
  error = _coerce_tool_error(raw_error)
199
184
  if error is None or not _should_retry_qflow_base(error):
200
185
  raise
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
186
+ return self._request(profile, "GET", f"/qingbi/charts/qflow/baseinfo/{chart_id}", chart_id=chart_id)
225
187
 
226
188
  @tool_cn_name("更新报表基础信息")
227
189
  def qingbi_report_update_base(self, *, profile: str, chart_id: str, payload: JSONObject) -> JSONObject:
@@ -234,45 +196,7 @@ class QingbiReportTools(ToolBase):
234
196
  def qingbi_report_get_config(self, *, profile: str, chart_id: str) -> JSONObject:
235
197
  """执行工具方法逻辑。"""
236
198
  self._require_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"))
199
+ return self._request(profile, "GET", f"/qingbi/charts/{chart_id}/configs", chart_id=chart_id)
276
200
 
277
201
  @tool_cn_name("更新报表配置")
278
202
  def qingbi_report_update_config(self, *, profile: str, chart_id: str, payload: JSONObject) -> JSONObject:
@@ -327,16 +251,6 @@ class QingbiReportTools(ToolBase):
327
251
  error = _coerce_tool_error(raw_error)
328
252
  if error is None or not _should_retry_asos_data(error):
329
253
  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
340
254
  return self._request(
341
255
  profile,
342
256
  "POST",
@@ -344,11 +258,6 @@ class QingbiReportTools(ToolBase):
344
258
  chart_id=chart_id,
345
259
  params=params,
346
260
  json_body=payload or {},
347
- warnings=[warning],
348
- verification={
349
- "qflow_data_route_loaded": False,
350
- "asos_data_route_loaded": True,
351
- },
352
261
  )
353
262
 
354
263
  @tool_cn_name("删除报表")
@@ -463,21 +372,3 @@ def _extract_sorted_chart_items(result: JSONValue) -> list[JSONObject]:
463
372
  if isinstance(result, list):
464
373
  return result
465
374
  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
- )