@qingflow-tech/qingflow-app-user-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.
Files changed (89) hide show
  1. package/README.md +9 -3
  2. package/docs/local-agent-install.md +54 -3
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/lib/runtime.mjs +304 -13
  6. package/npm/scripts/postinstall.mjs +1 -5
  7. package/package.json +3 -2
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +255 -0
  10. package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
  11. package/skills/qingflow-app-builder/references/create-app.md +149 -0
  12. package/skills/qingflow-app-builder/references/environments.md +63 -0
  13. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  14. package/skills/qingflow-app-builder/references/gotchas.md +107 -0
  15. package/skills/qingflow-app-builder/references/match-rules.md +114 -0
  16. package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
  17. package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
  18. package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
  19. package/skills/qingflow-app-builder/references/update-flow.md +158 -0
  20. package/skills/qingflow-app-builder/references/update-layout.md +68 -0
  21. package/skills/qingflow-app-builder/references/update-schema.md +72 -0
  22. package/skills/qingflow-app-builder/references/update-views.md +284 -0
  23. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  24. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  25. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  26. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  27. package/skills/qingflow-app-user/SKILL.md +12 -11
  28. package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
  29. package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
  30. package/skills/qingflow-app-user/references/record-patterns.md +5 -5
  31. package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
  32. package/skills/qingflow-mcp-setup/SKILL.md +113 -0
  33. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  34. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  35. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  36. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  37. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  38. package/skills/qingflow-record-analysis/SKILL.md +6 -7
  39. package/skills/qingflow-record-analysis/manifest.yaml +10 -0
  40. package/skills/qingflow-record-delete/SKILL.md +5 -3
  41. package/skills/qingflow-record-import/SKILL.md +6 -2
  42. package/skills/qingflow-record-insert/SKILL.md +48 -4
  43. package/skills/qingflow-record-insert/manifest.yaml +6 -0
  44. package/skills/qingflow-record-update/SKILL.md +36 -24
  45. package/skills/qingflow-task-ops/SKILL.md +25 -25
  46. package/skills/qingflow-task-ops/references/environments.md +0 -1
  47. package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
  48. package/src/qingflow_mcp/__main__.py +6 -2
  49. package/src/qingflow_mcp/builder_facade/models.py +41 -2
  50. package/src/qingflow_mcp/builder_facade/service.py +2743 -423
  51. package/src/qingflow_mcp/cli/commands/app.py +3 -16
  52. package/src/qingflow_mcp/cli/commands/builder.py +30 -4
  53. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  54. package/src/qingflow_mcp/cli/commands/imports.py +1 -1
  55. package/src/qingflow_mcp/cli/commands/record.py +54 -11
  56. package/src/qingflow_mcp/cli/context.py +0 -3
  57. package/src/qingflow_mcp/cli/formatters.py +238 -8
  58. package/src/qingflow_mcp/cli/main.py +47 -3
  59. package/src/qingflow_mcp/errors.py +43 -2
  60. package/src/qingflow_mcp/public_surface.py +24 -16
  61. package/src/qingflow_mcp/response_trim.py +119 -12
  62. package/src/qingflow_mcp/server.py +17 -14
  63. package/src/qingflow_mcp/server_app_builder.py +29 -7
  64. package/src/qingflow_mcp/server_app_user.py +23 -24
  65. package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
  66. package/src/qingflow_mcp/solution/executor.py +112 -15
  67. package/src/qingflow_mcp/tools/ai_builder_tools.py +497 -65
  68. package/src/qingflow_mcp/tools/app_tools.py +237 -51
  69. package/src/qingflow_mcp/tools/approval_tools.py +196 -34
  70. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  71. package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
  72. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  73. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  74. package/src/qingflow_mcp/tools/export_tools.py +230 -33
  75. package/src/qingflow_mcp/tools/file_tools.py +7 -3
  76. package/src/qingflow_mcp/tools/import_tools.py +293 -40
  77. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  78. package/src/qingflow_mcp/tools/package_tools.py +134 -8
  79. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  80. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  81. package/src/qingflow_mcp/tools/record_tools.py +2305 -442
  82. package/src/qingflow_mcp/tools/resource_read_tools.py +191 -39
  83. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  84. package/src/qingflow_mcp/tools/solution_tools.py +57 -15
  85. package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
  86. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  87. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  88. package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
  89. 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
- result = self.backend.request("GET", context, f"/tag/{tag_id}")
79
- 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
+ )
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
- 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}
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
- 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
+ )
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
- {"profile": profile, "ws_id": session_profile.selected_ws_id, "tag_id": tag_id, "result": result},
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": result.get("tagIcon") if result.get("tagIcon") is not None else permission_source.get("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": len(tag_items),
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
- 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
+ )