@josephyan/qingflow-app-builder-mcp 0.2.0-beta.7 → 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 +5 -3
- package/docs/local-agent-install.md +21 -5
- package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
- package/npm/lib/runtime.mjs +168 -12
- package/package.json +1 -1
- package/pyproject.toml +4 -1
- package/skills/qingflow-app-builder/SKILL.md +155 -22
- package/skills/qingflow-app-builder/references/create-app.md +51 -21
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +28 -1
- package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
- package/skills/qingflow-app-builder/references/tool-selection.md +47 -19
- package/skills/qingflow-app-builder/references/update-flow.md +112 -25
- package/skills/qingflow-app-builder/references/update-layout.md +11 -24
- package/skills/qingflow-app-builder/references/update-schema.md +1 -23
- package/skills/qingflow-app-builder/references/update-views.md +87 -21
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/backend_client.py +210 -0
- package/src/qingflow_mcp/builder_facade/models.py +1252 -3
- package/src/qingflow_mcp/builder_facade/service.py +11367 -2389
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +515 -0
- package/src/qingflow_mcp/cli/commands/common.py +62 -0
- package/src/qingflow_mcp/cli/commands/imports.py +96 -0
- package/src/qingflow_mcp/cli/commands/record.py +304 -0
- package/src/qingflow_mcp/cli/commands/task.py +89 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +355 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +149 -0
- package/src/qingflow_mcp/config.py +39 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/list_type_labels.py +24 -0
- package/src/qingflow_mcp/response_trim.py +668 -0
- package/src/qingflow_mcp/server.py +160 -18
- package/src/qingflow_mcp/server_app_builder.py +275 -68
- package/src/qingflow_mcp/server_app_user.py +219 -191
- package/src/qingflow_mcp/session_store.py +41 -1
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +43 -4
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
- package/src/qingflow_mcp/solution/executor.py +107 -11
- package/src/qingflow_mcp/solution/spec_models.py +2 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2032 -127
- package/src/qingflow_mcp/tools/app_tools.py +419 -12
- package/src/qingflow_mcp/tools/approval_tools.py +571 -72
- package/src/qingflow_mcp/tools/auth_tools.py +398 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +756 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +179 -0
- package/src/qingflow_mcp/tools/directory_tools.py +203 -31
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +1 -0
- package/src/qingflow_mcp/tools/import_tools.py +2150 -0
- package/src/qingflow_mcp/tools/package_tools.py +18 -4
- package/src/qingflow_mcp/tools/portal_tools.py +31 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +109 -7
- package/src/qingflow_mcp/tools/record_tools.py +9894 -1104
- package/src/qingflow_mcp/tools/solution_tools.py +115 -3
- package/src/qingflow_mcp/tools/task_context_tools.py +2040 -0
- package/src/qingflow_mcp/tools/task_tools.py +376 -225
- package/src/qingflow_mcp/tools/workspace_tools.py +163 -19
|
@@ -307,11 +307,20 @@ class AuthTools(ToolBase):
|
|
|
307
307
|
|
|
308
308
|
def auth_whoami(self, *, profile: str = DEFAULT_PROFILE) -> dict[str, Any]:
|
|
309
309
|
try:
|
|
310
|
-
session_profile,
|
|
310
|
+
session_profile, backend_session, context = self._require_context(profile, require_workspace=False)
|
|
311
311
|
except QingflowApiError as error:
|
|
312
312
|
self._handle_error(profile, error)
|
|
313
313
|
raise AssertionError("unreachable")
|
|
314
|
-
|
|
314
|
+
if self._should_refresh_identity_metadata(session_profile):
|
|
315
|
+
refreshed_profile = self._refresh_identity_metadata(
|
|
316
|
+
profile=profile,
|
|
317
|
+
session_profile=session_profile,
|
|
318
|
+
backend_session=backend_session,
|
|
319
|
+
context=context,
|
|
320
|
+
)
|
|
321
|
+
if refreshed_profile is not None:
|
|
322
|
+
session_profile = refreshed_profile
|
|
323
|
+
response = {
|
|
315
324
|
"profile": session_profile.profile,
|
|
316
325
|
"base_url": session_profile.base_url,
|
|
317
326
|
"qf_version": session_profile.qf_version,
|
|
@@ -324,6 +333,14 @@ class AuthTools(ToolBase):
|
|
|
324
333
|
"persisted": session_profile.persisted,
|
|
325
334
|
"request_route": self._request_route_payload(context),
|
|
326
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
|
|
327
344
|
|
|
328
345
|
def auth_logout(self, *, profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict[str, Any]:
|
|
329
346
|
if not self.sessions.has_profile(profile):
|
|
@@ -520,6 +537,385 @@ class AuthTools(ToolBase):
|
|
|
520
537
|
"qf_version_source": context.qf_version_source or ("context" if context.qf_version else "unknown"),
|
|
521
538
|
}
|
|
522
539
|
|
|
540
|
+
def _should_refresh_identity_metadata(self, session_profile) -> bool: # type: ignore[no-untyped-def]
|
|
541
|
+
return (
|
|
542
|
+
session_profile.uid == 0
|
|
543
|
+
or session_profile.email is None
|
|
544
|
+
or session_profile.nick_name is None
|
|
545
|
+
or session_profile.selected_ws_name is None
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
def _refresh_identity_metadata(
|
|
549
|
+
self,
|
|
550
|
+
*,
|
|
551
|
+
profile: str,
|
|
552
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
553
|
+
backend_session, # type: ignore[no-untyped-def]
|
|
554
|
+
context: BackendRequestContext,
|
|
555
|
+
):
|
|
556
|
+
try:
|
|
557
|
+
user_info, _ = self._fetch_user_info(
|
|
558
|
+
session_profile.base_url,
|
|
559
|
+
backend_session.token,
|
|
560
|
+
session_profile.selected_ws_id,
|
|
561
|
+
qf_version=session_profile.qf_version,
|
|
562
|
+
qf_version_source=session_profile.qf_version_source,
|
|
563
|
+
)
|
|
564
|
+
except QingflowApiError:
|
|
565
|
+
return None
|
|
566
|
+
|
|
567
|
+
ws_name = session_profile.selected_ws_name
|
|
568
|
+
if session_profile.selected_ws_id is not None:
|
|
569
|
+
workspace = self._fetch_workspace_with_name_fallback(
|
|
570
|
+
session_profile.base_url,
|
|
571
|
+
backend_session.token,
|
|
572
|
+
session_profile.selected_ws_id,
|
|
573
|
+
qf_version=session_profile.qf_version,
|
|
574
|
+
qf_version_source=session_profile.qf_version_source,
|
|
575
|
+
)
|
|
576
|
+
if isinstance(workspace, dict):
|
|
577
|
+
ws_name = (
|
|
578
|
+
str(workspace.get("workspaceName") or workspace.get("wsName") or workspace.get("remark") or "").strip()
|
|
579
|
+
or ws_name
|
|
580
|
+
)
|
|
581
|
+
email = user_info["email"] if "email" in user_info else session_profile.email
|
|
582
|
+
nick_name = (
|
|
583
|
+
user_info.get("nickName")
|
|
584
|
+
or user_info.get("displayName")
|
|
585
|
+
or user_info.get("name")
|
|
586
|
+
or session_profile.nick_name
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
uid = user_info.get("uid")
|
|
590
|
+
refreshed = self.sessions.update_profile_metadata(
|
|
591
|
+
profile,
|
|
592
|
+
uid=int(uid) if uid is not None else session_profile.uid,
|
|
593
|
+
email=email,
|
|
594
|
+
nick_name=nick_name,
|
|
595
|
+
selected_ws_id=session_profile.selected_ws_id,
|
|
596
|
+
selected_ws_name=ws_name,
|
|
597
|
+
)
|
|
598
|
+
return refreshed
|
|
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
|
+
|
|
850
|
+
def _fetch_workspace_with_name_fallback(
|
|
851
|
+
self,
|
|
852
|
+
base_url: str,
|
|
853
|
+
token: str,
|
|
854
|
+
ws_id: int,
|
|
855
|
+
*,
|
|
856
|
+
qf_version: str | None,
|
|
857
|
+
qf_version_source: str | None,
|
|
858
|
+
) -> dict[str, Any] | None:
|
|
859
|
+
try:
|
|
860
|
+
workspace = self._fetch_workspace(
|
|
861
|
+
base_url,
|
|
862
|
+
token,
|
|
863
|
+
ws_id,
|
|
864
|
+
qf_version=qf_version,
|
|
865
|
+
qf_version_source=qf_version_source,
|
|
866
|
+
)
|
|
867
|
+
except QingflowApiError:
|
|
868
|
+
workspace = None
|
|
869
|
+
if isinstance(workspace, dict):
|
|
870
|
+
workspace_name = str(workspace.get("workspaceName") or workspace.get("wsName") or "").strip()
|
|
871
|
+
if workspace_name:
|
|
872
|
+
return workspace
|
|
873
|
+
try:
|
|
874
|
+
fallback = self._fetch_workspace_from_list(
|
|
875
|
+
base_url,
|
|
876
|
+
token,
|
|
877
|
+
ws_id,
|
|
878
|
+
qf_version=qf_version,
|
|
879
|
+
qf_version_source=qf_version_source,
|
|
880
|
+
)
|
|
881
|
+
except QingflowApiError:
|
|
882
|
+
fallback = None
|
|
883
|
+
return fallback or workspace
|
|
884
|
+
|
|
885
|
+
def _fetch_workspace_from_list(
|
|
886
|
+
self,
|
|
887
|
+
base_url: str,
|
|
888
|
+
token: str,
|
|
889
|
+
ws_id: int,
|
|
890
|
+
*,
|
|
891
|
+
qf_version: str | None,
|
|
892
|
+
qf_version_source: str | None,
|
|
893
|
+
) -> dict[str, Any] | None:
|
|
894
|
+
payload = self.backend.request(
|
|
895
|
+
"POST",
|
|
896
|
+
BackendRequestContext(
|
|
897
|
+
base_url=base_url,
|
|
898
|
+
token=token,
|
|
899
|
+
ws_id=ws_id,
|
|
900
|
+
qf_version=qf_version,
|
|
901
|
+
qf_version_source=qf_version_source,
|
|
902
|
+
),
|
|
903
|
+
"/user/workspaceList/pageQuery",
|
|
904
|
+
json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2]},
|
|
905
|
+
)
|
|
906
|
+
workspaces = payload.get("list") if isinstance(payload, dict) else []
|
|
907
|
+
if not isinstance(workspaces, list):
|
|
908
|
+
return None
|
|
909
|
+
found = next(
|
|
910
|
+
(
|
|
911
|
+
item
|
|
912
|
+
for item in workspaces
|
|
913
|
+
if isinstance(item, dict) and item.get("wsId") == ws_id
|
|
914
|
+
),
|
|
915
|
+
None,
|
|
916
|
+
)
|
|
917
|
+
return found if isinstance(found, dict) else None
|
|
918
|
+
|
|
523
919
|
def _fetch_public_key(self, base_url: str, *, qf_version: str | None) -> str | None:
|
|
524
920
|
# Endpoints to try (order matters, lowercase 'pubkey' is for Public Cloud)
|
|
525
921
|
endpoints = ["/user/pubkey", "/api/user/pubkey", "/user/publicKey", "/api/user/publicKey"]
|