@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.
- package/README.md +5 -6
- package/docs/local-agent-install.md +6 -57
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
- package/npm/lib/runtime.mjs +101 -21
- package/npm/scripts/postinstall.mjs +10 -1
- package/package.json +2 -3
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +42 -104
- package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
- package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
- package/skills/qingflow-app-builder/references/create-app.md +11 -30
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +9 -31
- package/skills/qingflow-app-builder/references/match-rules.md +0 -15
- package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
- package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
- package/skills/qingflow-app-builder/references/update-flow.md +38 -22
- package/skills/qingflow-app-builder/references/update-schema.md +0 -14
- package/skills/qingflow-app-builder/references/update-views.md +14 -83
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/skills/qingflow-mcp-setup/SKILL.md +111 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +61 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-workflow-builder/SKILL.md +96 -0
- package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
- package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
- package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
- package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
- package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
- package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
- package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
- package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
- package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +2 -6
- package/src/qingflow_mcp/builder_facade/models.py +108 -485
- package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
- package/src/qingflow_mcp/cli/commands/builder.py +364 -322
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +3 -12
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +3 -3
- package/src/qingflow_mcp/cli/commands/portal.py +2 -2
- package/src/qingflow_mcp/cli/commands/record.py +27 -101
- package/src/qingflow_mcp/cli/commands/task.py +47 -28
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +16 -784
- package/src/qingflow_mcp/cli/main.py +33 -117
- package/src/qingflow_mcp/errors.py +2 -43
- package/src/qingflow_mcp/public_surface.py +17 -26
- package/src/qingflow_mcp/response_trim.py +17 -81
- package/src/qingflow_mcp/server.py +12 -14
- package/src/qingflow_mcp/server_app_builder.py +36 -79
- package/src/qingflow_mcp/server_app_user.py +16 -22
- package/src/qingflow_mcp/session_store.py +7 -11
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/executor.py +18 -245
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
- package/src/qingflow_mcp/tools/app_tools.py +43 -184
- package/src/qingflow_mcp/tools/approval_tools.py +35 -197
- package/src/qingflow_mcp/tools/auth_tools.py +16 -92
- package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
- package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
- package/src/qingflow_mcp/tools/directory_tools.py +72 -236
- package/src/qingflow_mcp/tools/export_tools.py +34 -244
- package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
- package/src/qingflow_mcp/tools/file_tools.py +3 -9
- package/src/qingflow_mcp/tools/import_tools.py +49 -336
- package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
- package/src/qingflow_mcp/tools/package_tools.py +6 -118
- package/src/qingflow_mcp/tools/portal_tools.py +3 -39
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
- package/src/qingflow_mcp/tools/record_tools.py +360 -1145
- package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
- package/src/qingflow_mcp/tools/role_tools.py +9 -80
- package/src/qingflow_mcp/tools/solution_tools.py +45 -59
- package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
- package/src/qingflow_mcp/tools/task_tools.py +29 -113
- package/src/qingflow_mcp/tools/view_tools.py +3 -106
- package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
- package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
- package/npm/bin/qingflow-skills.mjs +0 -5
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
- package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
- 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,
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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,
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
93
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
)
|