@josephyan/qingflow-cli 0.2.0-beta.68 → 0.2.0-beta.70
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 +337 -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 +23 -6
- 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,
|
|
@@ -155,16 +155,33 @@ class QingbiReportTools(ToolBase):
|
|
|
155
155
|
page_size_y: int | None = None,
|
|
156
156
|
) -> JSONObject:
|
|
157
157
|
self._require_chart_id(chart_id)
|
|
158
|
-
params = {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
params = {
|
|
159
|
+
"qfUUID": uuid4().hex,
|
|
160
|
+
# Match Qingflow's real BI/qflow read path defaults so a caller can
|
|
161
|
+
# fetch chart data without having to synthesize pagination first.
|
|
162
|
+
"pageNum": page_num if page_num is not None else 1,
|
|
163
|
+
"pageSize": page_size if page_size is not None else 20,
|
|
164
|
+
}
|
|
163
165
|
if page_num_y is not None:
|
|
164
166
|
params["pageNumY"] = page_num_y
|
|
165
167
|
if page_size_y is not None:
|
|
166
168
|
params["pageSizeY"] = page_size_y
|
|
167
|
-
|
|
169
|
+
if payload:
|
|
170
|
+
return self._request(
|
|
171
|
+
profile,
|
|
172
|
+
"POST",
|
|
173
|
+
f"/qingbi/charts/data/qflow/{chart_id}/detail",
|
|
174
|
+
chart_id=chart_id,
|
|
175
|
+
params=params,
|
|
176
|
+
json_body=payload,
|
|
177
|
+
)
|
|
178
|
+
return self._request(
|
|
179
|
+
profile,
|
|
180
|
+
"GET",
|
|
181
|
+
f"/qingbi/charts/data/qflow/{chart_id}",
|
|
182
|
+
chart_id=chart_id,
|
|
183
|
+
params=params,
|
|
184
|
+
)
|
|
168
185
|
|
|
169
186
|
def qingbi_report_delete(self, *, profile: str, chart_id: str) -> JSONObject:
|
|
170
187
|
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
|
|
|
@@ -139,21 +139,28 @@ class WorkspaceTools(ToolBase):
|
|
|
139
139
|
qf_version=workspace_qf_version,
|
|
140
140
|
qf_version_source="workspace_system_version",
|
|
141
141
|
)
|
|
142
|
+
active_context = BackendRequestContext(
|
|
143
|
+
base_url=session_profile.base_url,
|
|
144
|
+
token=context.token,
|
|
145
|
+
ws_id=session_profile.selected_ws_id,
|
|
146
|
+
qf_version=session_profile.qf_version,
|
|
147
|
+
qf_version_source=session_profile.qf_version_source,
|
|
148
|
+
)
|
|
149
|
+
workspace_version = self._workspace_version_summary(
|
|
150
|
+
self._fetch_workspace_account_info(active_context),
|
|
151
|
+
workspace_base_info=self._fetch_workspace_base_info(active_context),
|
|
152
|
+
workspace_detail=result,
|
|
153
|
+
)
|
|
142
154
|
return {
|
|
143
155
|
"profile": profile,
|
|
144
156
|
"selected_ws_id": session_profile.selected_ws_id,
|
|
145
157
|
"selected_ws_name": session_profile.selected_ws_name,
|
|
146
158
|
"workspace": result,
|
|
159
|
+
"workspace_version": workspace_version,
|
|
147
160
|
"qf_version": session_profile.qf_version,
|
|
148
161
|
"qf_version_source": session_profile.qf_version_source,
|
|
149
162
|
"request_route": self.backend.describe_route(
|
|
150
|
-
|
|
151
|
-
base_url=session_profile.base_url,
|
|
152
|
-
token=context.token,
|
|
153
|
-
ws_id=session_profile.selected_ws_id,
|
|
154
|
-
qf_version=session_profile.qf_version,
|
|
155
|
-
qf_version_source=session_profile.qf_version_source,
|
|
156
|
-
)
|
|
163
|
+
active_context
|
|
157
164
|
),
|
|
158
165
|
}
|
|
159
166
|
|
|
@@ -189,6 +196,116 @@ class WorkspaceTools(ToolBase):
|
|
|
189
196
|
normalized = str(value).strip()
|
|
190
197
|
return normalized or None
|
|
191
198
|
|
|
199
|
+
def _fetch_workspace_base_info(self, context: BackendRequestContext) -> dict[str, Any] | None:
|
|
200
|
+
try:
|
|
201
|
+
payload = self.backend.request("GET", context, "/ws/baseInfo")
|
|
202
|
+
except QingflowApiError:
|
|
203
|
+
return None
|
|
204
|
+
return payload if isinstance(payload, dict) else None
|
|
205
|
+
|
|
206
|
+
def _fetch_workspace_account_info(self, context: BackendRequestContext) -> dict[str, Any] | None:
|
|
207
|
+
try:
|
|
208
|
+
payload = self.backend.request("GET", context, "/ws/account")
|
|
209
|
+
except QingflowApiError:
|
|
210
|
+
return None
|
|
211
|
+
return payload if isinstance(payload, dict) else None
|
|
212
|
+
|
|
213
|
+
def _workspace_version_summary(
|
|
214
|
+
self,
|
|
215
|
+
payload: Any,
|
|
216
|
+
*,
|
|
217
|
+
workspace_base_info: Any,
|
|
218
|
+
workspace_detail: Any,
|
|
219
|
+
) -> dict[str, Any]:
|
|
220
|
+
account_info = payload if isinstance(payload, dict) else {}
|
|
221
|
+
base_info = workspace_base_info if isinstance(workspace_base_info, dict) else {}
|
|
222
|
+
detail_info = workspace_detail if isinstance(workspace_detail, dict) else {}
|
|
223
|
+
level_code = self._first_present_int(
|
|
224
|
+
account_info.get("accountLevel"),
|
|
225
|
+
base_info.get("accountLevel"),
|
|
226
|
+
detail_info.get("accountLevel"),
|
|
227
|
+
)
|
|
228
|
+
level_name = self._account_level_name(level_code)
|
|
229
|
+
return {
|
|
230
|
+
"level_code": level_code,
|
|
231
|
+
"level_name": level_name,
|
|
232
|
+
"display_name": self._account_level_display_name(level_name),
|
|
233
|
+
"being_trial": self._first_present_bool(
|
|
234
|
+
account_info.get("trial"),
|
|
235
|
+
base_info.get("trial"),
|
|
236
|
+
detail_info.get("trial"),
|
|
237
|
+
),
|
|
238
|
+
"expire_date": self._first_present_value(
|
|
239
|
+
account_info.get("expireDate"),
|
|
240
|
+
base_info.get("expireDate"),
|
|
241
|
+
detail_info.get("expireDate"),
|
|
242
|
+
),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
def _account_level_name(self, level_code: int | None) -> str | None:
|
|
246
|
+
mapping = {
|
|
247
|
+
0: "FREE",
|
|
248
|
+
10: "AIR",
|
|
249
|
+
20: "BASIC",
|
|
250
|
+
30: "TEAM",
|
|
251
|
+
35: "PROFESSIONAL",
|
|
252
|
+
40: "ENTERPRISE",
|
|
253
|
+
}
|
|
254
|
+
return mapping.get(level_code)
|
|
255
|
+
|
|
256
|
+
def _account_level_display_name(self, level_name: str | None) -> str | None:
|
|
257
|
+
mapping = {
|
|
258
|
+
"FREE": "免费版",
|
|
259
|
+
"AIR": "Air版",
|
|
260
|
+
"BASIC": "Pro版",
|
|
261
|
+
"TEAM": "团队版",
|
|
262
|
+
"PROFESSIONAL": "专业版",
|
|
263
|
+
"ENTERPRISE": "企业版",
|
|
264
|
+
}
|
|
265
|
+
return mapping.get(level_name)
|
|
266
|
+
|
|
267
|
+
def _coerce_int(self, value: Any) -> int | None:
|
|
268
|
+
if isinstance(value, bool) or value is None:
|
|
269
|
+
return None
|
|
270
|
+
if isinstance(value, int):
|
|
271
|
+
return value
|
|
272
|
+
try:
|
|
273
|
+
return int(str(value).strip())
|
|
274
|
+
except (TypeError, ValueError):
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
def _coerce_bool(self, value: Any) -> bool | None:
|
|
278
|
+
if isinstance(value, bool):
|
|
279
|
+
return value
|
|
280
|
+
if value is None:
|
|
281
|
+
return None
|
|
282
|
+
normalized = str(value).strip().lower()
|
|
283
|
+
if normalized in {"true", "1"}:
|
|
284
|
+
return True
|
|
285
|
+
if normalized in {"false", "0"}:
|
|
286
|
+
return False
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
def _first_present_int(self, *values: Any) -> int | None:
|
|
290
|
+
for value in values:
|
|
291
|
+
coerced = self._coerce_int(value)
|
|
292
|
+
if coerced is not None:
|
|
293
|
+
return coerced
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
def _first_present_bool(self, *values: Any) -> bool | None:
|
|
297
|
+
for value in values:
|
|
298
|
+
coerced = self._coerce_bool(value)
|
|
299
|
+
if coerced is not None:
|
|
300
|
+
return coerced
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
def _first_present_value(self, *values: Any) -> Any:
|
|
304
|
+
for value in values:
|
|
305
|
+
if value is not None:
|
|
306
|
+
return value
|
|
307
|
+
return None
|
|
308
|
+
|
|
192
309
|
def workspace_set_plugin_status(
|
|
193
310
|
self,
|
|
194
311
|
*,
|