@josephyan/qingflow-cli 0.2.0-beta.70 → 0.2.0-beta.71

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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-cli@0.2.0-beta.70
6
+ npm install @josephyan/qingflow-cli@0.2.0-beta.71
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@0.2.0-beta.70 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@0.2.0-beta.71 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-cli",
3
- "version": "0.2.0-beta.70",
3
+ "version": "0.2.0-beta.71",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b70"
7
+ version = "0.2.0b71"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2505,9 +2505,14 @@ class AiBuilderFacade:
2505
2505
  page_size_y: int | None = None,
2506
2506
  ) -> JSONObject:
2507
2507
  normalized_payload = deepcopy(data_payload) if isinstance(data_payload, dict) else {}
2508
+ warnings: list[dict[str, Any]] = []
2509
+ verification = {
2510
+ "chart_exists": True,
2511
+ "chart_data_loaded": True,
2512
+ "chart_config_loaded": True,
2513
+ }
2508
2514
  try:
2509
2515
  base = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
2510
- config = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id).get("result") or {}
2511
2516
  data = self.charts.qingbi_report_get_data(
2512
2517
  profile=profile,
2513
2518
  chart_id=chart_id,
@@ -2534,6 +2539,36 @@ class AiBuilderFacade:
2534
2539
  suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": chart_id}},
2535
2540
  )
2536
2541
 
2542
+ try:
2543
+ config = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id).get("result") or {}
2544
+ except (QingflowApiError, RuntimeError) as error:
2545
+ config_from_data = data.get("config") if isinstance(data, dict) else None
2546
+ if isinstance(config_from_data, dict):
2547
+ config = deepcopy(config_from_data)
2548
+ verification["chart_config_loaded"] = True
2549
+ warnings.append(
2550
+ _warning(
2551
+ "CHART_CONFIG_FALLBACK_FROM_DATA",
2552
+ "chart config endpoint is unavailable for this chart id; using config embedded in chart data instead",
2553
+ )
2554
+ )
2555
+ else:
2556
+ api_error = _coerce_api_error(error)
2557
+ return _failed_from_api_error(
2558
+ "CHART_GET_FAILED",
2559
+ api_error,
2560
+ normalized_args={
2561
+ "chart_id": chart_id,
2562
+ "data_payload": normalized_payload,
2563
+ "page_num": page_num,
2564
+ "page_size": page_size,
2565
+ "page_num_y": page_num_y,
2566
+ "page_size_y": page_size_y,
2567
+ },
2568
+ details={"chart_id": chart_id},
2569
+ suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": chart_id}},
2570
+ )
2571
+
2537
2572
  response = ChartGetResponse(
2538
2573
  chart_id=chart_id,
2539
2574
  base=deepcopy(base) if isinstance(base, dict) else {},
@@ -2559,8 +2594,8 @@ class AiBuilderFacade:
2559
2594
  "request_id": None,
2560
2595
  "suggested_next_call": None,
2561
2596
  "noop": False,
2562
- "warnings": [],
2563
- "verification": {"chart_exists": True, "chart_data_loaded": True},
2597
+ "warnings": warnings,
2598
+ "verification": verification,
2564
2599
  "verified": True,
2565
2600
  **response.model_dump(mode="json"),
2566
2601
  }
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from uuid import uuid4
4
5
 
5
6
  from mcp.server.fastmcp import FastMCP
@@ -18,6 +19,37 @@ def _qingbi_base_url(base_url: str) -> str:
18
19
  return normalized[:-4] if normalized.endswith("/api") else normalized
19
20
 
20
21
 
22
+ def _should_retry_qflow_base(error: QingflowApiError) -> bool:
23
+ return int(getattr(error, "backend_code", 0) or 0) == 81007
24
+
25
+
26
+ def _should_retry_asos_data(error: QingflowApiError) -> bool:
27
+ backend_code = int(getattr(error, "backend_code", 0) or 0)
28
+ http_status = getattr(error, "http_status", None)
29
+ return backend_code in {44011, 81007} or http_status == 404
30
+
31
+
32
+ def _coerce_tool_error(error: RuntimeError | QingflowApiError) -> QingflowApiError | None:
33
+ if isinstance(error, QingflowApiError):
34
+ return error
35
+ if not isinstance(error, RuntimeError):
36
+ return None
37
+ try:
38
+ payload = json.loads(str(error))
39
+ except Exception:
40
+ return None
41
+ if not isinstance(payload, dict):
42
+ return None
43
+ return QingflowApiError(
44
+ category=str(payload.get("category") or "runtime"),
45
+ message=str(payload.get("message") or str(error)),
46
+ backend_code=payload.get("backend_code"),
47
+ request_id=payload.get("request_id"),
48
+ http_status=payload.get("http_status"),
49
+ details=payload.get("details") if isinstance(payload.get("details"), dict) else None,
50
+ )
51
+
52
+
21
53
  class QingbiReportTools(ToolBase):
22
54
  def register(self, mcp: FastMCP) -> None:
23
55
  @mcp.tool()
@@ -127,7 +159,13 @@ class QingbiReportTools(ToolBase):
127
159
 
128
160
  def qingbi_report_get_base(self, *, profile: str, chart_id: str) -> JSONObject:
129
161
  self._require_chart_id(chart_id)
130
- return self._request(profile, "GET", f"/qingbi/charts/baseinfo/{chart_id}", chart_id=chart_id)
162
+ try:
163
+ return self._request(profile, "GET", f"/qingbi/charts/baseinfo/{chart_id}", chart_id=chart_id)
164
+ except (QingflowApiError, RuntimeError) as raw_error:
165
+ error = _coerce_tool_error(raw_error)
166
+ if error is None or not _should_retry_qflow_base(error):
167
+ raise
168
+ return self._request(profile, "GET", f"/qingbi/charts/qflow/baseinfo/{chart_id}", chart_id=chart_id)
131
169
 
132
170
  def qingbi_report_update_base(self, *, profile: str, chart_id: str, payload: JSONObject) -> JSONObject:
133
171
  self._require_chart_id(chart_id)
@@ -166,21 +204,34 @@ class QingbiReportTools(ToolBase):
166
204
  params["pageNumY"] = page_num_y
167
205
  if page_size_y is not None:
168
206
  params["pageSizeY"] = page_size_y
169
- if payload:
207
+ try:
208
+ if payload:
209
+ return self._request(
210
+ profile,
211
+ "POST",
212
+ f"/qingbi/charts/data/qflow/{chart_id}/detail",
213
+ chart_id=chart_id,
214
+ params=params,
215
+ json_body=payload,
216
+ )
170
217
  return self._request(
171
218
  profile,
172
- "POST",
173
- f"/qingbi/charts/data/qflow/{chart_id}/detail",
219
+ "GET",
220
+ f"/qingbi/charts/data/qflow/{chart_id}",
174
221
  chart_id=chart_id,
175
222
  params=params,
176
- json_body=payload,
177
223
  )
224
+ except (QingflowApiError, RuntimeError) as raw_error:
225
+ error = _coerce_tool_error(raw_error)
226
+ if error is None or not _should_retry_asos_data(error):
227
+ raise
178
228
  return self._request(
179
229
  profile,
180
- "GET",
181
- f"/qingbi/charts/data/qflow/{chart_id}",
230
+ "POST",
231
+ f"/qingbi/charts/data/qflow/{chart_id}/asos",
182
232
  chart_id=chart_id,
183
233
  params=params,
234
+ json_body=payload or {},
184
235
  )
185
236
 
186
237
  def qingbi_report_delete(self, *, profile: str, chart_id: str) -> JSONObject: