@josephyan/qingflow-app-user-mcp 0.2.0-beta.38 → 0.2.0-beta.39
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-app-user-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.39
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.39 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -589,6 +589,12 @@ class ChartUpsertPatch(StrictModel):
|
|
|
589
589
|
}
|
|
590
590
|
if normalized in aliases:
|
|
591
591
|
payload["chart_type"] = aliases[normalized]
|
|
592
|
+
if isinstance(payload.get("chart_id"), int):
|
|
593
|
+
payload["chart_id"] = str(payload["chart_id"])
|
|
594
|
+
if isinstance(payload.get("dimension_field_ids"), list):
|
|
595
|
+
payload["dimension_field_ids"] = [str(item) for item in payload["dimension_field_ids"] if item is not None and str(item).strip()]
|
|
596
|
+
if isinstance(payload.get("indicator_field_ids"), list):
|
|
597
|
+
payload["indicator_field_ids"] = [str(item) for item in payload["indicator_field_ids"] if item is not None and str(item).strip()]
|
|
592
598
|
return payload
|
|
593
599
|
|
|
594
600
|
|
|
@@ -598,6 +604,18 @@ class ChartApplyRequest(StrictModel):
|
|
|
598
604
|
remove_chart_ids: list[str] = Field(default_factory=list)
|
|
599
605
|
reorder_chart_ids: list[str] = Field(default_factory=list)
|
|
600
606
|
|
|
607
|
+
@model_validator(mode="before")
|
|
608
|
+
@classmethod
|
|
609
|
+
def normalize_ids(cls, value: Any) -> Any:
|
|
610
|
+
if not isinstance(value, dict):
|
|
611
|
+
return value
|
|
612
|
+
payload = dict(value)
|
|
613
|
+
for key in ("remove_chart_ids", "reorder_chart_ids"):
|
|
614
|
+
raw = payload.get(key)
|
|
615
|
+
if isinstance(raw, list):
|
|
616
|
+
payload[key] = [str(item) for item in raw if item is not None and str(item).strip()]
|
|
617
|
+
return payload
|
|
618
|
+
|
|
601
619
|
@model_validator(mode="after")
|
|
602
620
|
def validate_shape(self) -> "ChartApplyRequest":
|
|
603
621
|
if not self.upsert_charts and not self.remove_chart_ids and not self.reorder_chart_ids:
|
|
@@ -615,6 +633,34 @@ class PortalComponentPositionPatch(StrictModel):
|
|
|
615
633
|
mobile_w: int = Field(default=12, validation_alias=AliasChoices("mobile_w", "mobileW"))
|
|
616
634
|
mobile_h: int = Field(default=8, validation_alias=AliasChoices("mobile_h", "mobileH"))
|
|
617
635
|
|
|
636
|
+
@model_validator(mode="before")
|
|
637
|
+
@classmethod
|
|
638
|
+
def normalize_nested_layout(cls, value: Any) -> Any:
|
|
639
|
+
if not isinstance(value, dict):
|
|
640
|
+
return value
|
|
641
|
+
payload = dict(value)
|
|
642
|
+
pc = payload.pop("pc", None)
|
|
643
|
+
mobile = payload.pop("mobile", None)
|
|
644
|
+
if isinstance(pc, dict):
|
|
645
|
+
if "pc_x" not in payload and "x" in pc:
|
|
646
|
+
payload["pc_x"] = pc.get("x")
|
|
647
|
+
if "pc_y" not in payload and "y" in pc:
|
|
648
|
+
payload["pc_y"] = pc.get("y")
|
|
649
|
+
if "pc_w" not in payload and "cols" in pc:
|
|
650
|
+
payload["pc_w"] = pc.get("cols")
|
|
651
|
+
if "pc_h" not in payload and "rows" in pc:
|
|
652
|
+
payload["pc_h"] = pc.get("rows")
|
|
653
|
+
if isinstance(mobile, dict):
|
|
654
|
+
if "mobile_x" not in payload and "x" in mobile:
|
|
655
|
+
payload["mobile_x"] = mobile.get("x")
|
|
656
|
+
if "mobile_y" not in payload and "y" in mobile:
|
|
657
|
+
payload["mobile_y"] = mobile.get("y")
|
|
658
|
+
if "mobile_w" not in payload and "cols" in mobile:
|
|
659
|
+
payload["mobile_w"] = mobile.get("cols")
|
|
660
|
+
if "mobile_h" not in payload and "rows" in mobile:
|
|
661
|
+
payload["mobile_h"] = mobile.get("rows")
|
|
662
|
+
return payload
|
|
663
|
+
|
|
618
664
|
|
|
619
665
|
class PortalChartRefPatch(StrictModel):
|
|
620
666
|
app_key: str
|
|
@@ -2947,9 +2947,9 @@ class AiBuilderFacade:
|
|
|
2947
2947
|
if isinstance(item, dict) and item.get("fieldId")
|
|
2948
2948
|
}
|
|
2949
2949
|
existing_by_id = {
|
|
2950
|
-
|
|
2950
|
+
_extract_chart_identifier(item): deepcopy(item)
|
|
2951
2951
|
for item in existing_chart_items
|
|
2952
|
-
if isinstance(item, dict) and (item
|
|
2952
|
+
if isinstance(item, dict) and _extract_chart_identifier(item)
|
|
2953
2953
|
}
|
|
2954
2954
|
existing_by_name = {
|
|
2955
2955
|
str(item.get("chartName") or "").strip(): deepcopy(item)
|
|
@@ -2970,9 +2970,9 @@ class AiBuilderFacade:
|
|
|
2970
2970
|
existing = existing_by_id.get(str(patch.chart_id))
|
|
2971
2971
|
if existing is None:
|
|
2972
2972
|
existing = existing_by_name.get(patch.name)
|
|
2973
|
-
chart_id =
|
|
2973
|
+
chart_id = _extract_chart_identifier(existing or {}) or str(patch.chart_id or f"mcp_{uuid4().hex[:16]}")
|
|
2974
2974
|
existing_name = str((existing or {}).get("chartName") or "").strip()
|
|
2975
|
-
existing_type =
|
|
2975
|
+
existing_type = _normalize_backend_chart_type((existing or {}).get("chartType"))
|
|
2976
2976
|
target_type = _map_public_chart_type_to_backend(patch.chart_type)
|
|
2977
2977
|
if existing is None:
|
|
2978
2978
|
create_payload = {
|
|
@@ -2989,7 +2989,15 @@ class AiBuilderFacade:
|
|
|
2989
2989
|
"editAuthIncludeSubDept": True,
|
|
2990
2990
|
}
|
|
2991
2991
|
create_result = self.charts.qingbi_report_create(profile=profile, payload=create_payload).get("result") or {}
|
|
2992
|
-
created_chart_id =
|
|
2992
|
+
created_chart_id = _extract_chart_identifier(create_result or {})
|
|
2993
|
+
if not created_chart_id:
|
|
2994
|
+
refreshed_items = self.charts.qingbi_report_list(profile=profile, app_key=app_key).get("items") or []
|
|
2995
|
+
refreshed_existing = _find_chart_by_name(
|
|
2996
|
+
refreshed_items,
|
|
2997
|
+
chart_name=patch.name,
|
|
2998
|
+
chart_type=target_type,
|
|
2999
|
+
)
|
|
3000
|
+
created_chart_id = _extract_chart_identifier(refreshed_existing or {})
|
|
2993
3001
|
if created_chart_id:
|
|
2994
3002
|
chart_id = created_chart_id
|
|
2995
3003
|
created_ids.append(chart_id)
|
|
@@ -3035,7 +3043,7 @@ class AiBuilderFacade:
|
|
|
3035
3043
|
except (QingflowApiError, RuntimeError, ValueError) as error:
|
|
3036
3044
|
api_error = _coerce_api_error(error) if not isinstance(error, ValueError) else None
|
|
3037
3045
|
failure = {
|
|
3038
|
-
"chart_id": str(patch.chart_id or ""),
|
|
3046
|
+
"chart_id": str(locals().get("chart_id") or patch.chart_id or ""),
|
|
3039
3047
|
"name": patch.name,
|
|
3040
3048
|
"status": "failed",
|
|
3041
3049
|
"message": str(error),
|
|
@@ -3082,13 +3090,13 @@ class AiBuilderFacade:
|
|
|
3082
3090
|
failed_items.append(failure)
|
|
3083
3091
|
chart_results.append(failure)
|
|
3084
3092
|
|
|
3085
|
-
noop = not created_ids and not updated_ids and not removed_ids and not reordered
|
|
3093
|
+
noop = not created_ids and not updated_ids and not removed_ids and not reordered and not failed_items
|
|
3086
3094
|
try:
|
|
3087
3095
|
readback_items = self.charts.qingbi_report_list(profile=profile, app_key=app_key).get("items") or []
|
|
3088
3096
|
readback_ids = {
|
|
3089
|
-
|
|
3097
|
+
_extract_chart_identifier(item)
|
|
3090
3098
|
for item in readback_items
|
|
3091
|
-
if isinstance(item, dict) and (item
|
|
3099
|
+
if isinstance(item, dict) and _extract_chart_identifier(item)
|
|
3092
3100
|
}
|
|
3093
3101
|
verified = (
|
|
3094
3102
|
all(chart_id in readback_ids for chart_id in created_ids + updated_ids)
|
|
@@ -3096,9 +3104,9 @@ class AiBuilderFacade:
|
|
|
3096
3104
|
)
|
|
3097
3105
|
if request.reorder_chart_ids:
|
|
3098
3106
|
ordered_readback = [
|
|
3099
|
-
|
|
3107
|
+
_extract_chart_identifier(item)
|
|
3100
3108
|
for item in readback_items
|
|
3101
|
-
if isinstance(item, dict) and (item
|
|
3109
|
+
if isinstance(item, dict) and _extract_chart_identifier(item)
|
|
3102
3110
|
]
|
|
3103
3111
|
requested_existing = [chart_id for chart_id in request.reorder_chart_ids if chart_id in ordered_readback]
|
|
3104
3112
|
verified = verified and ordered_readback[: len(requested_existing)] == requested_existing
|
|
@@ -3123,10 +3131,10 @@ class AiBuilderFacade:
|
|
|
3123
3131
|
"backend_code": failed_items[0].get("backend_code"),
|
|
3124
3132
|
"http_status": failed_items[0].get("http_status"),
|
|
3125
3133
|
"noop": noop,
|
|
3126
|
-
"verification": {"charts_verified": verified, "readback_unavailable": readback_unavailable},
|
|
3134
|
+
"verification": {"charts_verified": False if failed_items else verified, "readback_unavailable": readback_unavailable},
|
|
3127
3135
|
"app_key": app_key,
|
|
3128
3136
|
"chart_results": chart_results,
|
|
3129
|
-
"verified": verified,
|
|
3137
|
+
"verified": False if failed_items else verified,
|
|
3130
3138
|
}
|
|
3131
3139
|
return {
|
|
3132
3140
|
"status": "success" if verified or noop else "partial_success",
|
|
@@ -3212,6 +3220,16 @@ class AiBuilderFacade:
|
|
|
3212
3220
|
)
|
|
3213
3221
|
update_payload["components"] = component_payload
|
|
3214
3222
|
self.portals.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
|
|
3223
|
+
self.portals.portal_update_base_info(
|
|
3224
|
+
profile=profile,
|
|
3225
|
+
dash_key=dash_key,
|
|
3226
|
+
payload={
|
|
3227
|
+
"dashName": update_payload.get("dashName"),
|
|
3228
|
+
"dashIcon": update_payload.get("dashIcon"),
|
|
3229
|
+
"auth": deepcopy(update_payload.get("auth")),
|
|
3230
|
+
"tags": deepcopy(update_payload.get("tags") or []),
|
|
3231
|
+
},
|
|
3232
|
+
)
|
|
3215
3233
|
draft_result = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") or {}
|
|
3216
3234
|
except (QingflowApiError, RuntimeError, ValueError) as error:
|
|
3217
3235
|
api_error = _coerce_api_error(error) if not isinstance(error, ValueError) else None
|
|
@@ -4258,6 +4276,61 @@ def _build_public_portal_base_payload(
|
|
|
4258
4276
|
return data
|
|
4259
4277
|
|
|
4260
4278
|
|
|
4279
|
+
def _extract_chart_identifier(chart: Any) -> str:
|
|
4280
|
+
if not isinstance(chart, dict):
|
|
4281
|
+
return ""
|
|
4282
|
+
return str(
|
|
4283
|
+
chart.get("chartId")
|
|
4284
|
+
or chart.get("biChartId")
|
|
4285
|
+
or chart.get("chartKey")
|
|
4286
|
+
or ""
|
|
4287
|
+
).strip()
|
|
4288
|
+
|
|
4289
|
+
|
|
4290
|
+
def _normalize_backend_chart_type(value: Any) -> str:
|
|
4291
|
+
raw = str(value or "").strip()
|
|
4292
|
+
if not raw:
|
|
4293
|
+
return ""
|
|
4294
|
+
by_code = {
|
|
4295
|
+
"1": "detail",
|
|
4296
|
+
"2": "summary",
|
|
4297
|
+
"3": "indicator",
|
|
4298
|
+
"4": "columnar",
|
|
4299
|
+
"5": "line",
|
|
4300
|
+
"6": "pie",
|
|
4301
|
+
"7": "funnel",
|
|
4302
|
+
"8": "radar",
|
|
4303
|
+
"9": "bar",
|
|
4304
|
+
"10": "scatter",
|
|
4305
|
+
"11": "ring",
|
|
4306
|
+
"12": "dualaxes",
|
|
4307
|
+
"13": "map",
|
|
4308
|
+
"14": "timeline",
|
|
4309
|
+
}
|
|
4310
|
+
return by_code.get(raw, raw.lower())
|
|
4311
|
+
|
|
4312
|
+
|
|
4313
|
+
def _find_chart_by_name(items: Any, *, chart_name: str, chart_type: str | None = None) -> dict[str, Any] | None:
|
|
4314
|
+
target_name = str(chart_name or "").strip()
|
|
4315
|
+
target_type = _normalize_backend_chart_type(chart_type)
|
|
4316
|
+
candidates: list[dict[str, Any]] = []
|
|
4317
|
+
if not isinstance(items, list) or not target_name:
|
|
4318
|
+
return None
|
|
4319
|
+
for item in items:
|
|
4320
|
+
if not isinstance(item, dict):
|
|
4321
|
+
continue
|
|
4322
|
+
item_name = str(item.get("chartName") or "").strip()
|
|
4323
|
+
if item_name != target_name:
|
|
4324
|
+
continue
|
|
4325
|
+
item_type = _normalize_backend_chart_type(item.get("chartType"))
|
|
4326
|
+
if target_type and item_type and item_type != target_type:
|
|
4327
|
+
continue
|
|
4328
|
+
candidates.append(item)
|
|
4329
|
+
if not candidates:
|
|
4330
|
+
return None
|
|
4331
|
+
return deepcopy(candidates[-1])
|
|
4332
|
+
|
|
4333
|
+
|
|
4261
4334
|
def _portal_position_payload(position: Any) -> dict[str, Any]:
|
|
4262
4335
|
return {
|
|
4263
4336
|
"pc": {
|
|
@@ -4332,7 +4405,7 @@ def _resolve_chart_reference(*, charts: QingbiReportTools, profile: str, ref: An
|
|
|
4332
4405
|
for item in items:
|
|
4333
4406
|
if not isinstance(item, dict):
|
|
4334
4407
|
continue
|
|
4335
|
-
item_id =
|
|
4408
|
+
item_id = _extract_chart_identifier(item)
|
|
4336
4409
|
item_name = str(item.get("chartName") or "").strip()
|
|
4337
4410
|
if chart_id and item_id == chart_id:
|
|
4338
4411
|
return {"chart_id": item_id, "chart_name": item_name, "app_key": app_key}
|
|
@@ -18,10 +18,18 @@ class PortalTools(ToolBase):
|
|
|
18
18
|
def portal_get(profile: str = DEFAULT_PROFILE, dash_key: str = "", being_draft: bool = False) -> JSONObject:
|
|
19
19
|
return self.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
|
|
20
20
|
|
|
21
|
+
@mcp.tool()
|
|
22
|
+
def portal_get_base_info(profile: str = DEFAULT_PROFILE, dash_key: str = "") -> JSONObject:
|
|
23
|
+
return self.portal_get_base_info(profile=profile, dash_key=dash_key)
|
|
24
|
+
|
|
21
25
|
@mcp.tool()
|
|
22
26
|
def portal_create(profile: str = DEFAULT_PROFILE, payload: JSONObject | None = None) -> JSONObject:
|
|
23
27
|
return self.portal_create(profile=profile, payload=payload or {})
|
|
24
28
|
|
|
29
|
+
@mcp.tool(description=self._high_risk_tool_description(operation="update", target="portal base settings"))
|
|
30
|
+
def portal_update_base_info(profile: str = DEFAULT_PROFILE, dash_key: str = "", payload: JSONObject | None = None) -> JSONObject:
|
|
31
|
+
return self.portal_update_base_info(profile=profile, dash_key=dash_key, payload=payload or {})
|
|
32
|
+
|
|
25
33
|
@mcp.tool(description=self._high_risk_tool_description(operation="update", target="portal configuration"))
|
|
26
34
|
def portal_update(profile: str = DEFAULT_PROFILE, dash_key: str = "", payload: JSONObject | None = None) -> JSONObject:
|
|
27
35
|
return self.portal_update(profile=profile, dash_key=dash_key, payload=payload or {})
|
|
@@ -59,6 +67,29 @@ class PortalTools(ToolBase):
|
|
|
59
67
|
|
|
60
68
|
return self._run(profile, runner)
|
|
61
69
|
|
|
70
|
+
def portal_get_base_info(self, *, profile: str, dash_key: str) -> JSONObject:
|
|
71
|
+
self._require_dash_key(dash_key)
|
|
72
|
+
|
|
73
|
+
def runner(session_profile, context):
|
|
74
|
+
result = self.backend.request("GET", context, f"/dash/{dash_key}/baseInfo")
|
|
75
|
+
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "dash_key": dash_key, "result": result}
|
|
76
|
+
|
|
77
|
+
return self._run(profile, runner)
|
|
78
|
+
|
|
79
|
+
def portal_update_base_info(self, *, profile: str, dash_key: str, payload: JSONObject) -> JSONObject:
|
|
80
|
+
self._require_dash_key(dash_key)
|
|
81
|
+
body = self._require_dict(payload)
|
|
82
|
+
|
|
83
|
+
def runner(session_profile, context):
|
|
84
|
+
result = self.backend.request("POST", context, f"/dash/{dash_key}/baseInfo", json_body=body)
|
|
85
|
+
return self._attach_human_review_notice(
|
|
86
|
+
{"profile": profile, "ws_id": session_profile.selected_ws_id, "dash_key": dash_key, "result": result},
|
|
87
|
+
operation="update",
|
|
88
|
+
target="portal base settings",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return self._run(profile, runner)
|
|
92
|
+
|
|
62
93
|
def portal_update(self, *, profile: str, dash_key: str, payload: JSONObject) -> JSONObject:
|
|
63
94
|
self._require_dash_key(dash_key)
|
|
64
95
|
body = self._require_dict(payload)
|