@josephyan/qingflow-cli 0.2.0-beta.69 → 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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/backend_client.py +0 -1
- package/src/qingflow_mcp/builder_facade/models.py +34 -0
- package/src/qingflow_mcp/builder_facade/service.py +372 -17
- package/src/qingflow_mcp/cli/commands/builder.py +248 -1
- package/src/qingflow_mcp/cli/commands/common.py +15 -0
- package/src/qingflow_mcp/cli/commands/imports.py +12 -2
- package/src/qingflow_mcp/cli/commands/record.py +132 -32
- package/src/qingflow_mcp/cli/commands/workspace.py +1 -1
- package/src/qingflow_mcp/cli/formatters.py +52 -2
- package/src/qingflow_mcp/cli/main.py +7 -5
- package/src/qingflow_mcp/response_trim.py +668 -0
- package/src/qingflow_mcp/server_app_builder.py +136 -8
- package/src/qingflow_mcp/server_app_user.py +55 -11
- package/src/qingflow_mcp/tools/ai_builder_tools.py +270 -5
- package/src/qingflow_mcp/tools/app_tools.py +29 -0
- package/src/qingflow_mcp/tools/auth_tools.py +259 -1
- package/src/qingflow_mcp/tools/import_tools.py +59 -7
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +75 -7
- package/src/qingflow_mcp/tools/record_tools.py +6 -12
- package/src/qingflow_mcp/tools/workspace_tools.py +124 -7
|
@@ -320,7 +320,7 @@ class AuthTools(ToolBase):
|
|
|
320
320
|
)
|
|
321
321
|
if refreshed_profile is not None:
|
|
322
322
|
session_profile = refreshed_profile
|
|
323
|
-
|
|
323
|
+
response = {
|
|
324
324
|
"profile": session_profile.profile,
|
|
325
325
|
"base_url": session_profile.base_url,
|
|
326
326
|
"qf_version": session_profile.qf_version,
|
|
@@ -333,6 +333,14 @@ class AuthTools(ToolBase):
|
|
|
333
333
|
"persisted": session_profile.persisted,
|
|
334
334
|
"request_route": self._request_route_payload(context),
|
|
335
335
|
}
|
|
336
|
+
member_info, member_warnings = self._workspace_member_info(
|
|
337
|
+
session_profile=session_profile,
|
|
338
|
+
backend_session=backend_session,
|
|
339
|
+
)
|
|
340
|
+
response.update(member_info)
|
|
341
|
+
if member_warnings:
|
|
342
|
+
response["warnings"] = member_warnings
|
|
343
|
+
return response
|
|
336
344
|
|
|
337
345
|
def auth_logout(self, *, profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict[str, Any]:
|
|
338
346
|
if not self.sessions.has_profile(profile):
|
|
@@ -589,6 +597,256 @@ class AuthTools(ToolBase):
|
|
|
589
597
|
)
|
|
590
598
|
return refreshed
|
|
591
599
|
|
|
600
|
+
def _workspace_member_info(
|
|
601
|
+
self,
|
|
602
|
+
*,
|
|
603
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
604
|
+
backend_session, # type: ignore[no-untyped-def]
|
|
605
|
+
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
606
|
+
default_payload = {
|
|
607
|
+
"departments": [],
|
|
608
|
+
"roles": [],
|
|
609
|
+
"permission_level": None,
|
|
610
|
+
}
|
|
611
|
+
ws_id = session_profile.selected_ws_id
|
|
612
|
+
if ws_id is None:
|
|
613
|
+
return default_payload, []
|
|
614
|
+
|
|
615
|
+
context = BackendRequestContext(
|
|
616
|
+
base_url=backend_session.base_url,
|
|
617
|
+
token=backend_session.token,
|
|
618
|
+
ws_id=ws_id,
|
|
619
|
+
qf_version=backend_session.qf_version,
|
|
620
|
+
qf_version_source=backend_session.qf_version_source,
|
|
621
|
+
)
|
|
622
|
+
permission_level = self._resolve_permission_level(
|
|
623
|
+
self._workspace_auth(context, ws_id=ws_id)
|
|
624
|
+
)
|
|
625
|
+
payload = dict(default_payload)
|
|
626
|
+
payload["permission_level"] = permission_level
|
|
627
|
+
|
|
628
|
+
member = self._lookup_current_member(
|
|
629
|
+
context=context,
|
|
630
|
+
uid=session_profile.uid,
|
|
631
|
+
email=session_profile.email,
|
|
632
|
+
nick_name=session_profile.nick_name,
|
|
633
|
+
)
|
|
634
|
+
if member is None:
|
|
635
|
+
return payload, [
|
|
636
|
+
{
|
|
637
|
+
"code": "CURRENT_MEMBER_PROFILE_UNAVAILABLE",
|
|
638
|
+
"message": (
|
|
639
|
+
"auth_whoami could not resolve current member departments and roles "
|
|
640
|
+
f"in workspace {ws_id}."
|
|
641
|
+
),
|
|
642
|
+
}
|
|
643
|
+
]
|
|
644
|
+
|
|
645
|
+
payload["departments"] = self._compact_departments(member)
|
|
646
|
+
payload["roles"] = self._compact_roles(member)
|
|
647
|
+
return payload, []
|
|
648
|
+
|
|
649
|
+
def _workspace_auth(self, context: BackendRequestContext, *, ws_id: int) -> int | None:
|
|
650
|
+
workspace = self._fetch_workspace_auth_from_detail(context, ws_id=ws_id)
|
|
651
|
+
if workspace is not None:
|
|
652
|
+
return workspace
|
|
653
|
+
return self._fetch_workspace_auth_from_list(context, ws_id=ws_id)
|
|
654
|
+
|
|
655
|
+
def _fetch_workspace_auth_from_detail(self, context: BackendRequestContext, *, ws_id: int) -> int | None:
|
|
656
|
+
try:
|
|
657
|
+
workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
|
|
658
|
+
except QingflowApiError:
|
|
659
|
+
return None
|
|
660
|
+
if not isinstance(workspace, dict):
|
|
661
|
+
return None
|
|
662
|
+
return self._coerce_auth_value(workspace.get("auth"))
|
|
663
|
+
|
|
664
|
+
def _fetch_workspace_auth_from_list(self, context: BackendRequestContext, *, ws_id: int) -> int | None:
|
|
665
|
+
try:
|
|
666
|
+
payload = self.backend.request(
|
|
667
|
+
"POST",
|
|
668
|
+
context,
|
|
669
|
+
"/user/workspaceList/pageQuery",
|
|
670
|
+
json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2, 3]},
|
|
671
|
+
)
|
|
672
|
+
except QingflowApiError:
|
|
673
|
+
return None
|
|
674
|
+
workspaces = payload.get("list") if isinstance(payload, dict) else []
|
|
675
|
+
if not isinstance(workspaces, list):
|
|
676
|
+
return None
|
|
677
|
+
for item in workspaces:
|
|
678
|
+
if not isinstance(item, dict) or item.get("wsId") != ws_id:
|
|
679
|
+
continue
|
|
680
|
+
return self._coerce_auth_value(item.get("auth"))
|
|
681
|
+
return None
|
|
682
|
+
|
|
683
|
+
def _lookup_current_member(
|
|
684
|
+
self,
|
|
685
|
+
*,
|
|
686
|
+
context: BackendRequestContext,
|
|
687
|
+
uid: int | None,
|
|
688
|
+
email: str | None,
|
|
689
|
+
nick_name: str | None,
|
|
690
|
+
) -> dict[str, Any] | None:
|
|
691
|
+
candidates: list[dict[str, Any]] = []
|
|
692
|
+
for keyword in (email, nick_name):
|
|
693
|
+
member = self._search_member_once(context, uid=uid, keyword=keyword)
|
|
694
|
+
if member is not None:
|
|
695
|
+
return member
|
|
696
|
+
if keyword:
|
|
697
|
+
candidates.extend(self._search_member_items(context, keyword=keyword))
|
|
698
|
+
if uid is not None and uid > 0:
|
|
699
|
+
for item in candidates:
|
|
700
|
+
if self._same_member(item, uid=uid):
|
|
701
|
+
return item
|
|
702
|
+
return self._search_member_once(context, uid=uid, keyword=None)
|
|
703
|
+
return None
|
|
704
|
+
|
|
705
|
+
def _search_member_once(
|
|
706
|
+
self,
|
|
707
|
+
context: BackendRequestContext,
|
|
708
|
+
*,
|
|
709
|
+
uid: int | None,
|
|
710
|
+
keyword: str | None,
|
|
711
|
+
) -> dict[str, Any] | None:
|
|
712
|
+
for item in self._search_member_items(context, keyword=keyword):
|
|
713
|
+
if self._same_member(item, uid=uid):
|
|
714
|
+
return item
|
|
715
|
+
return None
|
|
716
|
+
|
|
717
|
+
def _search_member_items(self, context: BackendRequestContext, *, keyword: str | None) -> list[dict[str, Any]]:
|
|
718
|
+
params: dict[str, Any] = {"pageNum": 1, "pageSize": 100, "containDisable": True}
|
|
719
|
+
normalized_keyword = str(keyword or "").strip()
|
|
720
|
+
if normalized_keyword:
|
|
721
|
+
params["keyword"] = normalized_keyword
|
|
722
|
+
try:
|
|
723
|
+
payload = self.backend.request("GET", context, "/contact", params=params)
|
|
724
|
+
except QingflowApiError:
|
|
725
|
+
return []
|
|
726
|
+
items = self._extract_items(payload)
|
|
727
|
+
return [item for item in items if isinstance(item, dict)]
|
|
728
|
+
|
|
729
|
+
def _same_member(self, item: dict[str, Any], *, uid: int | None) -> bool:
|
|
730
|
+
if uid is None or uid <= 0:
|
|
731
|
+
return False
|
|
732
|
+
for key in ("uid", "id", "userId"):
|
|
733
|
+
value = item.get(key)
|
|
734
|
+
if value is None:
|
|
735
|
+
continue
|
|
736
|
+
coerced = self._coerce_int(value)
|
|
737
|
+
if coerced is not None and coerced == uid:
|
|
738
|
+
return True
|
|
739
|
+
if str(value).strip() == str(uid):
|
|
740
|
+
return True
|
|
741
|
+
return False
|
|
742
|
+
|
|
743
|
+
def _compact_departments(self, member: dict[str, Any]) -> list[dict[str, Any]]:
|
|
744
|
+
items: list[dict[str, Any]] = []
|
|
745
|
+
seen: set[tuple[int | None, str | None]] = set()
|
|
746
|
+
for depart in self._walk_nested_items(member.get("departs")):
|
|
747
|
+
if not isinstance(depart, dict):
|
|
748
|
+
continue
|
|
749
|
+
dept_id = self._coerce_int(
|
|
750
|
+
depart.get("deptId", depart.get("departId", depart.get("id")))
|
|
751
|
+
)
|
|
752
|
+
dept_name = self._normalize_text(
|
|
753
|
+
depart.get("deptName", depart.get("departName", depart.get("name")))
|
|
754
|
+
)
|
|
755
|
+
key = (dept_id, dept_name)
|
|
756
|
+
if key in seen or (dept_id is None and dept_name is None):
|
|
757
|
+
continue
|
|
758
|
+
seen.add(key)
|
|
759
|
+
item = {"dept_id": dept_id, "dept_name": dept_name}
|
|
760
|
+
items.append({k: v for k, v in item.items() if v is not None})
|
|
761
|
+
return items
|
|
762
|
+
|
|
763
|
+
def _compact_roles(self, member: dict[str, Any]) -> list[dict[str, Any]]:
|
|
764
|
+
items: list[dict[str, Any]] = []
|
|
765
|
+
seen: set[tuple[int | None, str | None]] = set()
|
|
766
|
+
for role in self._walk_nested_items(member.get("roles")):
|
|
767
|
+
if not isinstance(role, dict):
|
|
768
|
+
continue
|
|
769
|
+
role_id = self._coerce_int(role.get("roleId", role.get("id")))
|
|
770
|
+
role_name = self._normalize_text(role.get("roleName", role.get("name")))
|
|
771
|
+
key = (role_id, role_name)
|
|
772
|
+
if key in seen or (role_id is None and role_name is None):
|
|
773
|
+
continue
|
|
774
|
+
seen.add(key)
|
|
775
|
+
item = {"role_id": role_id, "role_name": role_name}
|
|
776
|
+
items.append({k: v for k, v in item.items() if v is not None})
|
|
777
|
+
return items
|
|
778
|
+
|
|
779
|
+
def _resolve_permission_level(self, auth_code: int | None) -> str | None:
|
|
780
|
+
mapping = {
|
|
781
|
+
2: "超级管理",
|
|
782
|
+
1: "系统管理员",
|
|
783
|
+
3: "子管理员",
|
|
784
|
+
0: "基本成员",
|
|
785
|
+
}
|
|
786
|
+
return mapping.get(auth_code)
|
|
787
|
+
|
|
788
|
+
def _coerce_auth_value(self, value: Any) -> int | None:
|
|
789
|
+
coerced = self._coerce_int(value)
|
|
790
|
+
if coerced is not None:
|
|
791
|
+
return coerced
|
|
792
|
+
normalized = self._normalize_text(value)
|
|
793
|
+
if normalized is None:
|
|
794
|
+
return None
|
|
795
|
+
lowered = normalized.lower()
|
|
796
|
+
if lowered in {"creator", "workspaccreator", "workspacecreator"}:
|
|
797
|
+
return 2
|
|
798
|
+
if lowered in {"admin", "administrator"}:
|
|
799
|
+
return 1
|
|
800
|
+
if lowered in {"subadmin", "dataadmin"}:
|
|
801
|
+
return 3
|
|
802
|
+
if lowered in {"member", "visitor", "normal"}:
|
|
803
|
+
return 0
|
|
804
|
+
return None
|
|
805
|
+
|
|
806
|
+
def _extract_items(self, payload: Any) -> list[Any]:
|
|
807
|
+
if isinstance(payload, list):
|
|
808
|
+
return payload
|
|
809
|
+
if not isinstance(payload, dict):
|
|
810
|
+
return []
|
|
811
|
+
for key in ("list", "items", "rows", "result"):
|
|
812
|
+
value = payload.get(key)
|
|
813
|
+
if isinstance(value, list):
|
|
814
|
+
return value
|
|
815
|
+
for key in ("data", "page"):
|
|
816
|
+
nested = payload.get(key)
|
|
817
|
+
if isinstance(nested, list):
|
|
818
|
+
return nested
|
|
819
|
+
if isinstance(nested, dict):
|
|
820
|
+
for nested_key in ("list", "items", "rows", "result"):
|
|
821
|
+
value = nested.get(nested_key)
|
|
822
|
+
if isinstance(value, list):
|
|
823
|
+
return value
|
|
824
|
+
return []
|
|
825
|
+
|
|
826
|
+
def _walk_nested_items(self, value: Any) -> list[Any]:
|
|
827
|
+
if isinstance(value, list):
|
|
828
|
+
items: list[Any] = []
|
|
829
|
+
for item in value:
|
|
830
|
+
items.extend(self._walk_nested_items(item))
|
|
831
|
+
return items
|
|
832
|
+
return [value]
|
|
833
|
+
|
|
834
|
+
def _coerce_int(self, value: Any) -> int | None:
|
|
835
|
+
if isinstance(value, bool) or value is None:
|
|
836
|
+
return None
|
|
837
|
+
if isinstance(value, int):
|
|
838
|
+
return value
|
|
839
|
+
try:
|
|
840
|
+
return int(str(value).strip())
|
|
841
|
+
except (TypeError, ValueError):
|
|
842
|
+
return None
|
|
843
|
+
|
|
844
|
+
def _normalize_text(self, value: Any) -> str | None:
|
|
845
|
+
if value is None:
|
|
846
|
+
return None
|
|
847
|
+
text = str(value).strip()
|
|
848
|
+
return text or None
|
|
849
|
+
|
|
592
850
|
def _fetch_workspace_with_name_fallback(
|
|
593
851
|
self,
|
|
594
852
|
base_url: str,
|
|
@@ -129,6 +129,25 @@ class ImportTools(ToolBase):
|
|
|
129
129
|
import_id: str | None = None,
|
|
130
130
|
process_id_str: str | None = None,
|
|
131
131
|
) -> dict[str, Any]:
|
|
132
|
+
selector_count = sum(
|
|
133
|
+
1
|
|
134
|
+
for item in (
|
|
135
|
+
bool(_normalize_optional_text(process_id_str)),
|
|
136
|
+
bool(_normalize_optional_text(import_id)),
|
|
137
|
+
bool(str(app_key or "").strip()),
|
|
138
|
+
)
|
|
139
|
+
if item
|
|
140
|
+
)
|
|
141
|
+
if selector_count != 1:
|
|
142
|
+
return self._failed_status_result(
|
|
143
|
+
error_code="CONFIG_ERROR",
|
|
144
|
+
message="record_import_status_get accepts exactly one selector: process_id_str, import_id, or app_key",
|
|
145
|
+
extra={
|
|
146
|
+
"details": {
|
|
147
|
+
"fix_hint": "Use `process_id_str` or `import_id` for a known import, or use only `app_key` to inspect the latest import in that app.",
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
)
|
|
132
151
|
return self.record_import_status_get(
|
|
133
152
|
profile=profile,
|
|
134
153
|
app_key=app_key,
|
|
@@ -729,30 +748,62 @@ class ImportTools(ToolBase):
|
|
|
729
748
|
self,
|
|
730
749
|
*,
|
|
731
750
|
profile: str,
|
|
732
|
-
app_key: str,
|
|
751
|
+
app_key: str = "",
|
|
733
752
|
import_id: str | None = None,
|
|
734
753
|
process_id_str: str | None = None,
|
|
735
754
|
) -> dict[str, Any]:
|
|
736
|
-
|
|
737
|
-
|
|
755
|
+
normalized_app_key = (app_key or "").strip()
|
|
756
|
+
normalized_import_id = _normalize_optional_text(import_id)
|
|
757
|
+
normalized_process_id = _normalize_optional_text(process_id_str)
|
|
758
|
+
if normalized_import_id and normalized_process_id:
|
|
759
|
+
return self._failed_status_result(
|
|
760
|
+
error_code="CONFIG_ERROR",
|
|
761
|
+
message="record_import_status_get accepts import_id or process_id_str, but not both at the same time",
|
|
762
|
+
extra={
|
|
763
|
+
"details": {
|
|
764
|
+
"fix_hint": "Use only one of `import_id` or `process_id_str`. You may pass `app_key` as an optional routing hint for direct method compatibility.",
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
)
|
|
768
|
+
if not normalized_process_id and not normalized_import_id and not normalized_app_key:
|
|
769
|
+
return self._failed_status_result(
|
|
770
|
+
error_code="CONFIG_ERROR",
|
|
771
|
+
message="record_import_status_get requires at least one selector: process_id_str, import_id, or app_key",
|
|
772
|
+
extra={
|
|
773
|
+
"details": {
|
|
774
|
+
"fix_hint": "Use `process_id_str` or `import_id` for a known import, or use only `app_key` to inspect the latest import in that app.",
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
)
|
|
738
778
|
|
|
739
779
|
def runner(_session_profile, context):
|
|
740
780
|
local_job = None
|
|
741
|
-
normalized_import_id = _normalize_optional_text(import_id)
|
|
742
|
-
normalized_process_id = _normalize_optional_text(process_id_str)
|
|
743
781
|
if normalized_import_id:
|
|
744
782
|
local_job = self._job_store.get(normalized_import_id)
|
|
745
783
|
if local_job is None and normalized_process_id:
|
|
746
784
|
matches = [item for item in self._job_store.list() if _normalize_optional_text(item.get("process_id_str")) == normalized_process_id]
|
|
747
785
|
local_job = matches[0] if len(matches) == 1 else None
|
|
786
|
+
resolved_app_key = normalized_app_key
|
|
787
|
+
if not resolved_app_key and isinstance(local_job, dict):
|
|
788
|
+
resolved_app_key = str(local_job.get("app_key") or "").strip()
|
|
789
|
+
if not resolved_app_key:
|
|
790
|
+
return self._failed_status_result(
|
|
791
|
+
error_code="CONFIG_ERROR",
|
|
792
|
+
message="record_import_status_get could not determine app_key from the provided selector",
|
|
793
|
+
extra={
|
|
794
|
+
"details": {
|
|
795
|
+
"fix_hint": "Use the original `app_key`, or call import status with the latest-import mode: only `app_key`.",
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
)
|
|
748
799
|
if local_job is None and not normalized_import_id and not normalized_process_id:
|
|
749
|
-
recent = [item for item in self._job_store.list() if str(item.get("app_key")) ==
|
|
800
|
+
recent = [item for item in self._job_store.list() if str(item.get("app_key")) == resolved_app_key]
|
|
750
801
|
local_job = recent[0] if recent else None
|
|
751
802
|
page = self.backend.request(
|
|
752
803
|
"GET",
|
|
753
804
|
context,
|
|
754
805
|
"/app/apply/dataImport/record",
|
|
755
|
-
params={"appKey":
|
|
806
|
+
params={"appKey": resolved_app_key, "pageNum": 1, "pageSize": 100},
|
|
756
807
|
)
|
|
757
808
|
records = _extract_import_records(page)
|
|
758
809
|
matched_record, matched_by = _match_import_record(
|
|
@@ -785,6 +836,7 @@ class ImportTools(ToolBase):
|
|
|
785
836
|
return {
|
|
786
837
|
"ok": True,
|
|
787
838
|
"status": _normalize_optional_text(matched_record.get("processStatus")) or "unknown",
|
|
839
|
+
"app_key": resolved_app_key,
|
|
788
840
|
"import_id": normalized_import_id or (local_job.get("import_id") if isinstance(local_job, dict) else None),
|
|
789
841
|
"process_id_str": normalized_process,
|
|
790
842
|
"matched_by": matched_by,
|
|
@@ -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
|
-
|
|
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)
|
|
@@ -155,16 +193,46 @@ class QingbiReportTools(ToolBase):
|
|
|
155
193
|
page_size_y: int | None = None,
|
|
156
194
|
) -> JSONObject:
|
|
157
195
|
self._require_chart_id(chart_id)
|
|
158
|
-
params = {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
196
|
+
params = {
|
|
197
|
+
"qfUUID": uuid4().hex,
|
|
198
|
+
# Match Qingflow's real BI/qflow read path defaults so a caller can
|
|
199
|
+
# fetch chart data without having to synthesize pagination first.
|
|
200
|
+
"pageNum": page_num if page_num is not None else 1,
|
|
201
|
+
"pageSize": page_size if page_size is not None else 20,
|
|
202
|
+
}
|
|
163
203
|
if page_num_y is not None:
|
|
164
204
|
params["pageNumY"] = page_num_y
|
|
165
205
|
if page_size_y is not None:
|
|
166
206
|
params["pageSizeY"] = page_size_y
|
|
167
|
-
|
|
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
|
+
)
|
|
217
|
+
return self._request(
|
|
218
|
+
profile,
|
|
219
|
+
"GET",
|
|
220
|
+
f"/qingbi/charts/data/qflow/{chart_id}",
|
|
221
|
+
chart_id=chart_id,
|
|
222
|
+
params=params,
|
|
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
|
|
228
|
+
return self._request(
|
|
229
|
+
profile,
|
|
230
|
+
"POST",
|
|
231
|
+
f"/qingbi/charts/data/qflow/{chart_id}/asos",
|
|
232
|
+
chart_id=chart_id,
|
|
233
|
+
params=params,
|
|
234
|
+
json_body=payload or {},
|
|
235
|
+
)
|
|
168
236
|
|
|
169
237
|
def qingbi_report_delete(self, *, profile: str, chart_id: str) -> JSONObject:
|
|
170
238
|
self._require_chart_id(chart_id)
|
|
@@ -317,9 +317,6 @@ class RecordTools(ToolBase):
|
|
|
317
317
|
limit: int = 50,
|
|
318
318
|
strict_full: bool = True,
|
|
319
319
|
view_id: str | None = None,
|
|
320
|
-
list_type: int | None = None,
|
|
321
|
-
view_key: str | None = None,
|
|
322
|
-
view_name: str | None = None,
|
|
323
320
|
output_profile: str = "normal",
|
|
324
321
|
) -> JSONObject:
|
|
325
322
|
return self.record_analyze(
|
|
@@ -332,9 +329,9 @@ class RecordTools(ToolBase):
|
|
|
332
329
|
limit=limit,
|
|
333
330
|
strict_full=strict_full,
|
|
334
331
|
view_id=view_id,
|
|
335
|
-
list_type=
|
|
336
|
-
view_key=
|
|
337
|
-
view_name=
|
|
332
|
+
list_type=None,
|
|
333
|
+
view_key=None,
|
|
334
|
+
view_name=None,
|
|
338
335
|
output_profile=output_profile,
|
|
339
336
|
)
|
|
340
337
|
|
|
@@ -354,9 +351,6 @@ class RecordTools(ToolBase):
|
|
|
354
351
|
limit: int = 50,
|
|
355
352
|
page: int = 1,
|
|
356
353
|
view_id: str | None = None,
|
|
357
|
-
list_type: int | None = None,
|
|
358
|
-
view_key: str | None = None,
|
|
359
|
-
view_name: str | None = None,
|
|
360
354
|
output_profile: str = "normal",
|
|
361
355
|
) -> JSONObject:
|
|
362
356
|
return self.record_list(
|
|
@@ -368,9 +362,9 @@ class RecordTools(ToolBase):
|
|
|
368
362
|
limit=limit,
|
|
369
363
|
page=page,
|
|
370
364
|
view_id=view_id,
|
|
371
|
-
list_type=
|
|
372
|
-
view_key=
|
|
373
|
-
view_name=
|
|
365
|
+
list_type=None,
|
|
366
|
+
view_key=None,
|
|
367
|
+
view_name=None,
|
|
374
368
|
output_profile=output_profile,
|
|
375
369
|
)
|
|
376
370
|
|