@qingflow-tech/qingflow-app-builder-mcp 1.0.10 → 1.0.12
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 +6 -3
- package/docs/local-agent-install.md +54 -3
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/lib/runtime.mjs +304 -13
- package/npm/scripts/postinstall.mjs +1 -5
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +12 -12
- package/skills/qingflow-app-builder/references/create-app.md +3 -3
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +1 -1
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +6 -5
- package/skills/qingflow-app-builder/references/update-views.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -3
- 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/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +41 -2
- package/src/qingflow_mcp/builder_facade/service.py +2743 -423
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +30 -4
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +1 -1
- package/src/qingflow_mcp/cli/commands/record.py +54 -11
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +238 -8
- package/src/qingflow_mcp/cli/main.py +47 -3
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +24 -16
- package/src/qingflow_mcp/response_trim.py +119 -12
- package/src/qingflow_mcp/server.py +17 -14
- package/src/qingflow_mcp/server_app_builder.py +29 -7
- package/src/qingflow_mcp/server_app_user.py +23 -24
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
- package/src/qingflow_mcp/solution/executor.py +112 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +497 -65
- package/src/qingflow_mcp/tools/app_tools.py +237 -51
- package/src/qingflow_mcp/tools/approval_tools.py +196 -34
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +230 -33
- package/src/qingflow_mcp/tools/file_tools.py +7 -3
- package/src/qingflow_mcp/tools/import_tools.py +293 -40
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +134 -8
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +2305 -442
- package/src/qingflow_mcp/tools/resource_read_tools.py +191 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +57 -15
- package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
|
@@ -5,8 +5,9 @@ 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
|
+
from ..solution.compiler.icon_utils import workspace_icon_config
|
|
10
11
|
from .base import ToolBase, tool_cn_name
|
|
11
12
|
|
|
12
13
|
|
|
@@ -75,8 +76,44 @@ class PackageTools(ToolBase):
|
|
|
75
76
|
self._require_tag_id(tag_id)
|
|
76
77
|
|
|
77
78
|
def runner(session_profile, context):
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
)
|
|
80
117
|
compact = self._compact_package(
|
|
81
118
|
result if isinstance(result, dict) else {},
|
|
82
119
|
base_info=base_info if isinstance(base_info, dict) else None,
|
|
@@ -87,7 +124,11 @@ class PackageTools(ToolBase):
|
|
|
87
124
|
"tag_id": tag_id,
|
|
88
125
|
"result": result if include_raw else compact,
|
|
89
126
|
"compact": not include_raw,
|
|
127
|
+
"detail_degraded": bool(warnings),
|
|
128
|
+
"verification": verification,
|
|
90
129
|
}
|
|
130
|
+
if warnings:
|
|
131
|
+
response["warnings"] = warnings
|
|
91
132
|
if include_raw:
|
|
92
133
|
response["summary"] = compact
|
|
93
134
|
return response
|
|
@@ -100,7 +141,27 @@ class PackageTools(ToolBase):
|
|
|
100
141
|
self._require_readable_tag_id(tag_id)
|
|
101
142
|
|
|
102
143
|
def runner(session_profile, context):
|
|
103
|
-
|
|
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
165
|
compact = self._compact_package(result if isinstance(result, dict) else {})
|
|
105
166
|
response = {
|
|
106
167
|
"profile": profile,
|
|
@@ -108,7 +169,10 @@ class PackageTools(ToolBase):
|
|
|
108
169
|
"tag_id": tag_id,
|
|
109
170
|
"result": result if include_raw else compact,
|
|
110
171
|
"compact": not include_raw,
|
|
172
|
+
"verification": verification,
|
|
111
173
|
}
|
|
174
|
+
if warnings:
|
|
175
|
+
response["warnings"] = warnings
|
|
112
176
|
if include_raw:
|
|
113
177
|
response["summary"] = compact
|
|
114
178
|
return response
|
|
@@ -133,15 +197,44 @@ class PackageTools(ToolBase):
|
|
|
133
197
|
body = self._require_dict(payload)
|
|
134
198
|
|
|
135
199
|
def runner(session_profile, context):
|
|
136
|
-
|
|
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
227
|
result = self.backend.request(
|
|
138
228
|
"PUT",
|
|
139
229
|
context,
|
|
140
230
|
f"/tag/{tag_id}",
|
|
141
231
|
json_body=self._normalize_package_payload(body, existing=current),
|
|
142
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
|
|
143
236
|
return self._attach_human_review_notice(
|
|
144
|
-
|
|
237
|
+
response,
|
|
145
238
|
operation="update",
|
|
146
239
|
target="app package settings",
|
|
147
240
|
)
|
|
@@ -250,6 +343,19 @@ class PackageTools(ToolBase):
|
|
|
250
343
|
if tag_id < 0:
|
|
251
344
|
raise_tool_error(QingflowApiError.config_error("tag_id must be non-negative"))
|
|
252
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
|
+
|
|
253
359
|
def _normalize_package_payload(self, payload: JSONObject, existing: JSONObject | None = None) -> JSONObject:
|
|
254
360
|
"""执行内部辅助逻辑。"""
|
|
255
361
|
data = deepcopy(existing) if isinstance(existing, dict) else {}
|
|
@@ -281,13 +387,26 @@ class PackageTools(ToolBase):
|
|
|
281
387
|
"dashKey": item.get("dashKey"),
|
|
282
388
|
}
|
|
283
389
|
)
|
|
390
|
+
item_count = None
|
|
391
|
+
raw_item_count = result.get("itemCount")
|
|
392
|
+
try:
|
|
393
|
+
if raw_item_count is not None:
|
|
394
|
+
coerced_count = int(raw_item_count)
|
|
395
|
+
if coerced_count >= 0:
|
|
396
|
+
item_count = coerced_count
|
|
397
|
+
except (TypeError, ValueError):
|
|
398
|
+
item_count = None
|
|
399
|
+
if item_count is None and "tagItems" in result:
|
|
400
|
+
item_count = len(tag_items)
|
|
401
|
+
tag_icon = result.get("tagIcon") if result.get("tagIcon") is not None else permission_source.get("tagIcon")
|
|
284
402
|
compact = {
|
|
285
403
|
"tagId": result.get("tagId"),
|
|
286
404
|
"tagName": result.get("tagName"),
|
|
287
|
-
"tagIcon":
|
|
405
|
+
"tagIcon": tag_icon,
|
|
406
|
+
"iconConfig": workspace_icon_config(str(tag_icon).strip() if tag_icon not in (None, "") else None),
|
|
288
407
|
"publishStatus": result.get("publishStatus") if result.get("publishStatus") is not None else permission_source.get("publishStatus"),
|
|
289
408
|
"beingTrial": result.get("beingTrial"),
|
|
290
|
-
"itemCount":
|
|
409
|
+
"itemCount": item_count,
|
|
291
410
|
"itemPreview": preview,
|
|
292
411
|
"addAppStatus": permission_source.get("addAppStatus"),
|
|
293
412
|
"editAppStatus": permission_source.get("editAppStatus"),
|
|
@@ -324,3 +443,10 @@ def _default_package_auth() -> JSONObject:
|
|
|
324
443
|
"contactAuth": {"type": "WORKSPACE_ALL", "authMembers": deepcopy(members)},
|
|
325
444
|
"externalMemberAuth": {"type": "NOT", "authMembers": deepcopy(members)},
|
|
326
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
)
|