@josephyan/qingflow-cli 0.2.0-beta.78 → 0.2.0-beta.79

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.
@@ -58,14 +58,17 @@ from .models import (
58
58
  PortalReadSummaryResponse,
59
59
  PortalSectionPatch,
60
60
  PublicDepartmentScopeMode,
61
+ PublicExternalVisibilityMode,
61
62
  PublicFieldType,
62
63
  PublicRelationMode,
63
64
  PublicChartType,
64
65
  PublicButtonTriggerAction,
66
+ PublicVisibilityMode,
65
67
  PublicViewType,
66
68
  PublicViewButtonConfigType,
67
69
  PublicViewButtonType,
68
70
  SchemaPlanRequest,
71
+ VisibilityPatch,
69
72
  ViewButtonBindingPatch,
70
73
  ViewUpsertPatch,
71
74
  ViewFilterOperator,
@@ -175,6 +178,16 @@ class RelationHydrationResult:
175
178
  degraded_expectations: list[dict[str, Any]] = field(default_factory=list)
176
179
 
177
180
 
181
+ @dataclass(slots=True)
182
+ class VisibilityResolutionError(Exception):
183
+ error_code: str
184
+ message: str
185
+ details: JSONObject = field(default_factory=dict)
186
+
187
+ def __str__(self) -> str:
188
+ return self.message
189
+
190
+
178
191
  class AiBuilderFacade:
179
192
  def __init__(
180
193
  self,
@@ -272,12 +285,14 @@ class AiBuilderFacade:
272
285
  package_name: str,
273
286
  icon: str | None = None,
274
287
  color: str | None = None,
288
+ visibility: VisibilityPatch | None = None,
275
289
  ) -> JSONObject:
276
290
  requested = str(package_name or "").strip()
277
291
  normalized_args = {
278
292
  "package_name": requested,
279
293
  **({"icon": icon} if icon else {}),
280
294
  **({"color": color} if color else {}),
295
+ **({"visibility": visibility.model_dump(mode="json")} if visibility is not None else {}),
281
296
  }
282
297
  if not requested:
283
298
  return _failed(
@@ -292,6 +307,16 @@ class AiBuilderFacade:
292
307
  title=requested,
293
308
  fallback_icon_name="files-folder",
294
309
  )
310
+ try:
311
+ desired_auth = self._compile_visibility_to_member_auth(profile=profile, visibility=visibility)
312
+ except VisibilityResolutionError as error:
313
+ return _failed(
314
+ error.error_code,
315
+ error.message,
316
+ normalized_args=normalized_args,
317
+ details=error.details,
318
+ suggested_next_call=None,
319
+ )
295
320
  existing = self.package_resolve(profile=profile, package_name=requested)
296
321
  lookup_permission_blocked = None
297
322
  if existing.get("status") == "success":
@@ -327,7 +352,7 @@ class AiBuilderFacade:
327
352
  try:
328
353
  created = self.packages.package_create(
329
354
  profile=profile,
330
- payload={"tagName": requested, "tagIcon": desired_tag_icon},
355
+ payload={"tagName": requested, "tagIcon": desired_tag_icon, "auth": desired_auth},
331
356
  )
332
357
  except (QingflowApiError, RuntimeError) as error:
333
358
  api_error = _coerce_api_error(error)
@@ -378,6 +403,169 @@ class AiBuilderFacade:
378
403
  "tag_id": tag_id,
379
404
  "tag_name": tag_name,
380
405
  "tag_icon": tag_icon,
406
+ "visibility": _public_visibility_from_member_auth(desired_auth),
407
+ }
408
+
409
+ def package_get(self, *, profile: str, tag_id: int) -> JSONObject:
410
+ normalized_args = {"tag_id": tag_id}
411
+ if _coerce_positive_int(tag_id) is None:
412
+ return _failed("TAG_ID_REQUIRED", "tag_id must be positive", normalized_args=normalized_args, suggested_next_call=None)
413
+ try:
414
+ detail_result = self.packages.package_get(profile=profile, tag_id=tag_id, include_raw=True)
415
+ base_result = self.packages.package_get_base(profile=profile, tag_id=tag_id, include_raw=True)
416
+ except (QingflowApiError, RuntimeError) as error:
417
+ api_error = _coerce_api_error(error)
418
+ return _failed_from_api_error(
419
+ "PACKAGE_GET_FAILED",
420
+ api_error,
421
+ normalized_args=normalized_args,
422
+ details={"tag_id": tag_id},
423
+ suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "tag_id": tag_id}},
424
+ )
425
+ detail = detail_result.get("result") if isinstance(detail_result.get("result"), dict) else {}
426
+ base = base_result.get("result") if isinstance(base_result.get("result"), dict) else {}
427
+ summary = detail_result.get("summary") if isinstance(detail_result.get("summary"), dict) else {}
428
+ return {
429
+ "status": "success",
430
+ "error_code": None,
431
+ "recoverable": False,
432
+ "message": "read package detail",
433
+ "normalized_args": normalized_args,
434
+ "missing_fields": [],
435
+ "allowed_values": {},
436
+ "details": {},
437
+ "request_id": None,
438
+ "suggested_next_call": None,
439
+ "noop": False,
440
+ "warnings": [],
441
+ "verification": {"package_exists": True},
442
+ "verified": True,
443
+ "tag_id": _coerce_positive_int(detail.get("tagId") or base.get("tagId")) or tag_id,
444
+ "tag_name": str(detail.get("tagName") or base.get("tagName") or "").strip() or None,
445
+ "tag_icon": str(detail.get("tagIcon") or base.get("tagIcon") or "").strip() or None,
446
+ "publish_status": detail.get("publishStatus") if detail.get("publishStatus") is not None else base.get("publishStatus"),
447
+ "item_count": summary.get("itemCount"),
448
+ "item_preview": deepcopy(summary.get("itemPreview") or []),
449
+ "add_app_status": base.get("addAppStatus") if base.get("addAppStatus") is not None else summary.get("addAppStatus"),
450
+ "edit_app_status": base.get("editAppStatus") if base.get("editAppStatus") is not None else summary.get("editAppStatus"),
451
+ "del_app_status": base.get("delAppStatus") if base.get("delAppStatus") is not None else summary.get("delAppStatus"),
452
+ "edit_tag_status": base.get("editTagStatus") if base.get("editTagStatus") is not None else summary.get("editTagStatus"),
453
+ "visibility": _public_visibility_from_member_auth(base.get("auth") or detail.get("auth")),
454
+ }
455
+
456
+ def package_update(
457
+ self,
458
+ *,
459
+ profile: str,
460
+ tag_id: int,
461
+ package_name: str | None = None,
462
+ icon: str | None = None,
463
+ color: str | None = None,
464
+ visibility: VisibilityPatch | None = None,
465
+ ) -> JSONObject:
466
+ normalized_args = {
467
+ "tag_id": tag_id,
468
+ **({"package_name": package_name} if str(package_name or "").strip() else {}),
469
+ **({"icon": icon} if icon else {}),
470
+ **({"color": color} if color else {}),
471
+ **({"visibility": visibility.model_dump(mode="json")} if visibility is not None else {}),
472
+ }
473
+ if _coerce_positive_int(tag_id) is None:
474
+ return _failed("TAG_ID_REQUIRED", "tag_id must be positive", normalized_args=normalized_args, suggested_next_call=None)
475
+ try:
476
+ current = self.packages.package_get(profile=profile, tag_id=tag_id, include_raw=True)
477
+ current_base = self.packages.package_get_base(profile=profile, tag_id=tag_id, include_raw=True)
478
+ except (QingflowApiError, RuntimeError) as error:
479
+ api_error = _coerce_api_error(error)
480
+ return _failed_from_api_error(
481
+ "PACKAGE_UPDATE_FAILED",
482
+ api_error,
483
+ normalized_args=normalized_args,
484
+ details={"tag_id": tag_id},
485
+ suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "tag_id": tag_id}},
486
+ )
487
+ raw_current = current.get("result") if isinstance(current.get("result"), dict) else {}
488
+ raw_current_base = current_base.get("result") if isinstance(current_base.get("result"), dict) else {}
489
+ current_name = str(raw_current.get("tagName") or "").strip() or None
490
+ desired_name = str(package_name or current_name or "").strip() or current_name or "未命名应用包"
491
+ desired_icon = encode_workspace_icon_with_defaults(
492
+ icon=icon,
493
+ color=color,
494
+ title=desired_name,
495
+ fallback_icon_name="files-folder",
496
+ existing_icon=str(raw_current.get("tagIcon") or raw_current_base.get("tagIcon") or "").strip() or None,
497
+ )
498
+ try:
499
+ desired_auth = (
500
+ self._compile_visibility_to_member_auth(profile=profile, visibility=visibility)
501
+ if visibility is not None
502
+ else deepcopy(raw_current_base.get("auth") or raw_current.get("auth") or default_member_auth())
503
+ )
504
+ except VisibilityResolutionError as error:
505
+ return _failed(
506
+ error.error_code,
507
+ error.message,
508
+ normalized_args=normalized_args,
509
+ details=error.details,
510
+ suggested_next_call=None,
511
+ )
512
+ try:
513
+ update_result = self.packages.package_update(
514
+ profile=profile,
515
+ tag_id=tag_id,
516
+ payload={"tagName": desired_name, "tagIcon": desired_icon, "auth": desired_auth},
517
+ )
518
+ except (QingflowApiError, RuntimeError) as error:
519
+ api_error = _coerce_api_error(error)
520
+ return _failed_from_api_error(
521
+ "PACKAGE_UPDATE_FAILED",
522
+ api_error,
523
+ normalized_args=normalized_args,
524
+ details={"tag_id": tag_id},
525
+ suggested_next_call={"tool_name": "package_update", "arguments": {"profile": profile, **normalized_args}},
526
+ )
527
+ verification = self.package_get(profile=profile, tag_id=tag_id)
528
+ if verification.get("status") != "success":
529
+ return verification
530
+ return {
531
+ "status": "success",
532
+ "error_code": None,
533
+ "recoverable": False,
534
+ "message": "updated package",
535
+ "normalized_args": normalized_args,
536
+ "missing_fields": [],
537
+ "allowed_values": {},
538
+ "details": {},
539
+ "request_id": update_result.get("request_id") if isinstance(update_result, dict) else None,
540
+ "suggested_next_call": None,
541
+ "noop": False,
542
+ "warnings": [],
543
+ "verification": {
544
+ "package_exists": True,
545
+ "visibility_verified": verification.get("visibility") == _public_visibility_from_member_auth(desired_auth),
546
+ },
547
+ "verified": True,
548
+ **{
549
+ key: deepcopy(value)
550
+ for key, value in verification.items()
551
+ if key
552
+ not in {
553
+ "status",
554
+ "error_code",
555
+ "recoverable",
556
+ "message",
557
+ "normalized_args",
558
+ "missing_fields",
559
+ "allowed_values",
560
+ "details",
561
+ "request_id",
562
+ "suggested_next_call",
563
+ "noop",
564
+ "warnings",
565
+ "verification",
566
+ "verified",
567
+ }
568
+ },
381
569
  }
382
570
 
383
571
  def solution_install(
@@ -789,6 +977,294 @@ class AiBuilderFacade:
789
977
 
790
978
  return {"member_uids": [item["uid"] for item in resolved], "member_entries": resolved, "issues": issues}
791
979
 
980
+ def _resolve_department_references(
981
+ self,
982
+ *,
983
+ profile: str,
984
+ dept_ids: list[int],
985
+ dept_names: list[str],
986
+ ) -> dict[str, Any]:
987
+ issues: list[dict[str, Any]] = []
988
+ resolved: list[dict[str, Any]] = []
989
+ seen_ids: set[int] = set()
990
+ listed = self.directory.directory_list_all_departments(
991
+ profile=profile,
992
+ parent_dept_id=None,
993
+ max_depth=20,
994
+ max_items=5000,
995
+ )
996
+ items = _extract_directory_items(listed)
997
+ by_id: dict[int, dict[str, Any]] = {}
998
+ by_name: dict[str, list[dict[str, Any]]] = {}
999
+ for item in items:
1000
+ dept_id = _coerce_positive_int(item.get("deptId") or item.get("id"))
1001
+ if dept_id is None:
1002
+ continue
1003
+ by_id[dept_id] = item
1004
+ dept_name = str(item.get("deptName") or item.get("departName") or item.get("name") or "").strip()
1005
+ if dept_name:
1006
+ by_name.setdefault(dept_name, []).append(item)
1007
+
1008
+ def add_department(item: dict[str, Any], *, fallback_name: str | None = None) -> None:
1009
+ dept_id = _coerce_positive_int(item.get("deptId") or item.get("id"))
1010
+ if dept_id is None or dept_id in seen_ids:
1011
+ return
1012
+ resolved.append(
1013
+ {
1014
+ "deptId": dept_id,
1015
+ "deptName": str(item.get("deptName") or item.get("departName") or item.get("name") or fallback_name or dept_id),
1016
+ "deptIcon": item.get("deptIcon"),
1017
+ "beingFrontendConfig": True,
1018
+ }
1019
+ )
1020
+ seen_ids.add(dept_id)
1021
+
1022
+ for dept_id in dept_ids:
1023
+ normalized = _coerce_positive_int(dept_id)
1024
+ if normalized is None:
1025
+ continue
1026
+ add_department(by_id.get(normalized) or {"deptId": normalized}, fallback_name=str(normalized))
1027
+
1028
+ for dept_name in dept_names:
1029
+ requested = str(dept_name or "").strip()
1030
+ if not requested:
1031
+ continue
1032
+ exact = [item for item in (by_name.get(requested) or []) if isinstance(item, dict)]
1033
+ if len(exact) != 1:
1034
+ issues.append(
1035
+ {
1036
+ "kind": "department_name",
1037
+ "value": requested,
1038
+ "error_code": "AMBIGUOUS_DEPARTMENT" if len(exact) > 1 else "DEPARTMENT_NOT_FOUND",
1039
+ "matches": exact,
1040
+ }
1041
+ )
1042
+ continue
1043
+ add_department(exact[0], fallback_name=requested)
1044
+
1045
+ return {"department_entries": resolved, "issues": issues}
1046
+
1047
+ def _resolve_external_member_references(
1048
+ self,
1049
+ *,
1050
+ profile: str,
1051
+ member_ids: list[int],
1052
+ member_emails: list[str],
1053
+ ) -> dict[str, Any]:
1054
+ issues: list[dict[str, Any]] = []
1055
+ resolved: list[dict[str, Any]] = []
1056
+ seen_ids: set[int] = set()
1057
+
1058
+ def _external_member_id(item: dict[str, Any]) -> int | None:
1059
+ return _coerce_positive_int(item.get("uid") or item.get("memberId") or item.get("id"))
1060
+
1061
+ def add_member(item: dict[str, Any], *, fallback_name: str | None = None) -> None:
1062
+ member_id = _external_member_id(item)
1063
+ if member_id is None or member_id in seen_ids:
1064
+ return
1065
+ resolved.append(
1066
+ {
1067
+ "uid": member_id,
1068
+ "remark": item.get("remark") or item.get("nickName") or item.get("name") or fallback_name or str(member_id),
1069
+ "nickName": item.get("nickName") or item.get("name") or fallback_name or str(member_id),
1070
+ "email": item.get("email"),
1071
+ "headImg": item.get("headImg") or item.get("avatar"),
1072
+ "beingFrontendConfig": True,
1073
+ }
1074
+ )
1075
+ seen_ids.add(member_id)
1076
+
1077
+ for member_id in member_ids:
1078
+ normalized = _coerce_positive_int(member_id)
1079
+ if normalized is not None and normalized not in seen_ids:
1080
+ add_member({"uid": normalized}, fallback_name=str(normalized))
1081
+
1082
+ for member_email in member_emails:
1083
+ requested = str(member_email or "").strip()
1084
+ if not requested:
1085
+ continue
1086
+ listed = self.directory.directory_list_external_members(
1087
+ profile=profile,
1088
+ keyword=requested,
1089
+ page_num=1,
1090
+ page_size=100,
1091
+ simple=True,
1092
+ )
1093
+ items = _extract_directory_items(listed)
1094
+ exact = [
1095
+ item
1096
+ for item in items
1097
+ if isinstance(item, dict) and str(item.get("email") or "").strip().lower() == requested.lower()
1098
+ ]
1099
+ if len(exact) != 1:
1100
+ issues.append(
1101
+ {
1102
+ "kind": "external_member_email",
1103
+ "value": requested,
1104
+ "error_code": "AMBIGUOUS_EXTERNAL_MEMBER" if len(exact) > 1 else "EXTERNAL_MEMBER_NOT_FOUND",
1105
+ "matches": exact,
1106
+ }
1107
+ )
1108
+ continue
1109
+ add_member(exact[0], fallback_name=requested)
1110
+
1111
+ return {"member_entries": resolved, "issues": issues}
1112
+
1113
+ def _raise_visibility_resolution_issues(self, issues: list[dict[str, Any]]) -> None:
1114
+ if not issues:
1115
+ return
1116
+ first_issue = issues[0] if isinstance(issues[0], dict) else {}
1117
+ error_code = str(first_issue.get("error_code") or "VISIBILITY_SUBJECT_NOT_FOUND")
1118
+ raw_value = first_issue.get("value")
1119
+ requested_value = str(raw_value or "").strip()
1120
+ kind = str(first_issue.get("kind") or "subject")
1121
+ if error_code.startswith("AMBIGUOUS_"):
1122
+ public_code = "VISIBILITY_SUBJECT_AMBIGUOUS"
1123
+ message = f"{kind} '{requested_value}' matched multiple visibility subjects"
1124
+ elif error_code.endswith("_NOT_FOUND"):
1125
+ public_code = "VISIBILITY_SUBJECT_NOT_FOUND"
1126
+ message = f"{kind} '{requested_value}' was not found in the visibility directory"
1127
+ else:
1128
+ public_code = "VISIBILITY_SUBJECT_UNSUPPORTED"
1129
+ message = f"{kind} visibility selector is unsupported"
1130
+ raise VisibilityResolutionError(
1131
+ public_code,
1132
+ message,
1133
+ details={"issues": issues},
1134
+ )
1135
+
1136
+ def _compile_visibility_to_member_auth(
1137
+ self,
1138
+ *,
1139
+ profile: str,
1140
+ visibility: VisibilityPatch | None,
1141
+ ) -> dict[str, Any]:
1142
+ if visibility is None:
1143
+ return default_member_auth()
1144
+ if visibility.mode == PublicVisibilityMode.everyone:
1145
+ auth = default_member_auth()
1146
+ auth["type"] = "ALL"
1147
+ auth["contactAuth"]["type"] = "WORKSPACE_ALL"
1148
+ auth["externalMemberAuth"]["type"] = "WORKSPACE_ALL"
1149
+ auth["contactAuth"]["authMembers"]["includeSubDeparts"] = None
1150
+ auth["externalMemberAuth"]["authMembers"]["includeSubDeparts"] = None
1151
+ return auth
1152
+
1153
+ member_resolution = self._resolve_member_references(
1154
+ profile=profile,
1155
+ member_uids=visibility.selectors.member_uids,
1156
+ member_emails=visibility.selectors.member_emails,
1157
+ member_names=visibility.selectors.member_names,
1158
+ )
1159
+ role_resolution = self._resolve_role_references(
1160
+ profile=profile,
1161
+ role_ids=visibility.selectors.role_ids,
1162
+ role_names=visibility.selectors.role_names,
1163
+ )
1164
+ department_resolution = self._resolve_department_references(
1165
+ profile=profile,
1166
+ dept_ids=visibility.selectors.dept_ids,
1167
+ dept_names=visibility.selectors.dept_names,
1168
+ )
1169
+ external_member_resolution = self._resolve_external_member_references(
1170
+ profile=profile,
1171
+ member_ids=visibility.external_selectors.member_ids,
1172
+ member_emails=visibility.external_selectors.member_emails,
1173
+ )
1174
+ self._raise_visibility_resolution_issues(
1175
+ [
1176
+ *member_resolution["issues"],
1177
+ *role_resolution["issues"],
1178
+ *department_resolution["issues"],
1179
+ *external_member_resolution["issues"],
1180
+ ]
1181
+ )
1182
+
1183
+ external_depart_entries: list[dict[str, Any]] = []
1184
+ for dept_id in visibility.external_selectors.dept_ids:
1185
+ normalized = _coerce_positive_int(dept_id)
1186
+ if normalized is None:
1187
+ continue
1188
+ external_depart_entries.append(
1189
+ {
1190
+ "deptId": normalized,
1191
+ "deptName": str(normalized),
1192
+ "beingFrontendConfig": True,
1193
+ }
1194
+ )
1195
+
1196
+ contact_auth_type = "WORKSPACE_ALL"
1197
+ if visibility.mode == PublicVisibilityMode.specific:
1198
+ contact_auth_type = "SPECIFIC"
1199
+ external_auth_type = {
1200
+ PublicExternalVisibilityMode.not_: "NOT",
1201
+ PublicExternalVisibilityMode.workspace: "WORKSPACE_ALL",
1202
+ PublicExternalVisibilityMode.specific: "SPECIFIC",
1203
+ }[visibility.external_mode or PublicExternalVisibilityMode.not_]
1204
+ return {
1205
+ "type": "WORKSPACE",
1206
+ "contactAuth": {
1207
+ "type": contact_auth_type,
1208
+ "authMembers": {
1209
+ "member": deepcopy(member_resolution["member_entries"]),
1210
+ "depart": deepcopy(department_resolution["department_entries"]),
1211
+ "role": deepcopy(role_resolution["role_entries"]),
1212
+ "dynamic": [],
1213
+ "includeSubDeparts": visibility.selectors.include_sub_departs,
1214
+ },
1215
+ },
1216
+ "externalMemberAuth": {
1217
+ "type": external_auth_type,
1218
+ "authMembers": {
1219
+ "member": deepcopy(external_member_resolution["member_entries"]),
1220
+ "depart": deepcopy(external_depart_entries),
1221
+ "role": [],
1222
+ "dynamic": [],
1223
+ "includeSubDeparts": None,
1224
+ },
1225
+ },
1226
+ }
1227
+
1228
+ def _compile_visibility_to_chart_visible_auth(
1229
+ self,
1230
+ *,
1231
+ profile: str,
1232
+ visibility: VisibilityPatch | None,
1233
+ ) -> dict[str, Any]:
1234
+ if visibility is None:
1235
+ return qingbi_workspace_visible_auth()
1236
+
1237
+ member_auth = self._compile_visibility_to_member_auth(profile=profile, visibility=visibility)
1238
+ workspace_type = str(member_auth.get("type") or "").strip().upper()
1239
+ contact_auth = member_auth.get("contactAuth") if isinstance(member_auth.get("contactAuth"), dict) else {}
1240
+ external_auth = member_auth.get("externalMemberAuth") if isinstance(member_auth.get("externalMemberAuth"), dict) else {}
1241
+ contact_members = contact_auth.get("authMembers") if isinstance(contact_auth.get("authMembers"), dict) else {}
1242
+ external_members = external_auth.get("authMembers") if isinstance(external_auth.get("authMembers"), dict) else {}
1243
+ return {
1244
+ "type": "all" if workspace_type == "ALL" else "ws",
1245
+ "contactAuth": {
1246
+ "type": "assign" if str(contact_auth.get("type") or "").strip().upper() == "SPECIFIC" else "all",
1247
+ "authMembers": {
1248
+ "member": deepcopy(contact_members.get("member") or []),
1249
+ "depart": deepcopy(contact_members.get("depart") or []),
1250
+ "role": deepcopy(contact_members.get("role") or []),
1251
+ "includeSubDeparts": contact_members.get("includeSubDeparts"),
1252
+ },
1253
+ },
1254
+ "externalMemberAuth": {
1255
+ "type": {
1256
+ "NOT": "not",
1257
+ "WORKSPACE_ALL": "all",
1258
+ "SPECIFIC": "assign",
1259
+ }.get(str(external_auth.get("type") or "").strip().upper(), "not"),
1260
+ "authMembers": {
1261
+ "member": deepcopy(external_members.get("member") or []),
1262
+ "depart": deepcopy(external_members.get("depart") or []),
1263
+ "role": [],
1264
+ },
1265
+ },
1266
+ }
1267
+
792
1268
  def _normalize_flow_nodes(
793
1269
  self,
794
1270
  *,
@@ -2117,6 +2593,7 @@ class AiBuilderFacade:
2117
2593
  app_key=app_key,
2118
2594
  title=state["base"].get("formTitle"),
2119
2595
  app_icon=str(state["base"].get("appIcon") or "").strip() or None,
2596
+ visibility=_public_visibility_from_member_auth(state["base"].get("auth")),
2120
2597
  tag_ids=_coerce_int_list(state["base"].get("tagIds")),
2121
2598
  publish_status=state["base"].get("appPublishStatus"),
2122
2599
  field_count=len(parsed["fields"]),
@@ -2655,6 +3132,29 @@ class AiBuilderFacade:
2655
3132
  suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
2656
3133
  )
2657
3134
  charts = _summarize_charts(items)
3135
+ chart_visibility_read_errors: list[dict[str, Any]] = []
3136
+ for chart in charts:
3137
+ chart_id = str(chart.get("chart_id") or "").strip()
3138
+ if not chart_id:
3139
+ continue
3140
+ try:
3141
+ base_info = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
3142
+ except (QingflowApiError, RuntimeError) as error:
3143
+ api_error = _coerce_api_error(error)
3144
+ chart_visibility_read_errors.append(
3145
+ {
3146
+ "chart_id": chart_id,
3147
+ "request_id": api_error.request_id,
3148
+ "http_status": api_error.http_status,
3149
+ "backend_code": api_error.backend_code,
3150
+ }
3151
+ )
3152
+ continue
3153
+ chart["visibility_summary"] = _visibility_summary(
3154
+ _public_visibility_from_chart_visible_auth(
3155
+ base_info.get("visibleAuth") if isinstance(base_info, dict) else None
3156
+ )
3157
+ )
2658
3158
  response = AppChartsReadResponse(
2659
3159
  app_key=resolved_app_key,
2660
3160
  charts=charts,
@@ -2668,12 +3168,24 @@ class AiBuilderFacade:
2668
3168
  "normalized_args": {"app_key": resolved_app_key},
2669
3169
  "missing_fields": [],
2670
3170
  "allowed_values": {},
2671
- "details": {},
3171
+ "details": {"chart_visibility_read_errors": chart_visibility_read_errors} if chart_visibility_read_errors else {},
2672
3172
  "request_id": None,
2673
3173
  "suggested_next_call": None,
2674
3174
  "noop": False,
2675
- "warnings": [] if list_source == "sorted" else [_warning("CHART_ORDER_UNVERIFIED", "chart summary order uses fallback listing and may not reflect saved chart sort order")],
2676
- "verification": {"app_exists": True, "chart_order_verified": list_source == "sorted", "chart_list_source": list_source},
3175
+ "warnings": (
3176
+ ([] if list_source == "sorted" else [_warning("CHART_ORDER_UNVERIFIED", "chart summary order uses fallback listing and may not reflect saved chart sort order")])
3177
+ + (
3178
+ [_warning("CHART_VISIBILITY_READ_PARTIAL", "some chart base infos could not be read back; visibility_summary is incomplete for those charts")]
3179
+ if chart_visibility_read_errors
3180
+ else []
3181
+ )
3182
+ ),
3183
+ "verification": {
3184
+ "app_exists": True,
3185
+ "chart_order_verified": list_source == "sorted",
3186
+ "chart_list_source": list_source,
3187
+ "chart_visibility_readback_complete": not chart_visibility_read_errors,
3188
+ },
2677
3189
  "verified": True,
2678
3190
  **response.model_dump(mode="json"),
2679
3191
  }
@@ -2791,6 +3303,7 @@ class AiBuilderFacade:
2791
3303
  ],
2792
3304
  dash_icon=str(result.get("dashIcon") or "").strip() or None,
2793
3305
  hide_copyright=bool(result.get("hideCopyright")) if "hideCopyright" in result else None,
3306
+ visibility=_public_visibility_from_member_auth(result.get("auth")),
2794
3307
  auth=deepcopy(result.get("auth")) if isinstance(result.get("auth"), dict) else {},
2795
3308
  config=deepcopy(result.get("config")) if isinstance(result.get("config"), dict) else {},
2796
3309
  dash_global_config=deepcopy(result.get("dashGlobalConfig")) if isinstance(result.get("dashGlobalConfig"), dict) else {},
@@ -2916,6 +3429,7 @@ class AiBuilderFacade:
2916
3429
  response = ViewGetResponse(
2917
3430
  view_key=view_key,
2918
3431
  base_info=base_info,
3432
+ visibility=_public_visibility_from_member_auth(base_info.get("auth")),
2919
3433
  config=deepcopy(config) if isinstance(config, dict) else {},
2920
3434
  questions=questions,
2921
3435
  associations=associations,
@@ -2993,6 +3507,7 @@ class AiBuilderFacade:
2993
3507
  response = ChartGetResponse(
2994
3508
  chart_id=chart_id,
2995
3509
  base=deepcopy(base) if isinstance(base, dict) else {},
3510
+ visibility=_public_visibility_from_chart_visible_auth(base.get("visibleAuth")),
2996
3511
  config=deepcopy(config) if isinstance(config, dict) else {},
2997
3512
  )
2998
3513
  return {
@@ -3453,6 +3968,7 @@ class AiBuilderFacade:
3453
3968
  app_name: str = "",
3454
3969
  icon: str | None = None,
3455
3970
  color: str | None = None,
3971
+ visibility: VisibilityPatch | None = None,
3456
3972
  create_if_missing: bool = False,
3457
3973
  publish: bool = True,
3458
3974
  add_fields: list[FieldPatch],
@@ -3465,12 +3981,27 @@ class AiBuilderFacade:
3465
3981
  "app_name": app_name,
3466
3982
  "icon": icon,
3467
3983
  "color": color,
3984
+ "visibility": visibility.model_dump(mode="json") if visibility is not None else None,
3468
3985
  "create_if_missing": create_if_missing,
3469
3986
  "publish": publish,
3470
3987
  "add_fields": [patch.model_dump(mode="json") for patch in add_fields],
3471
3988
  "update_fields": [patch.model_dump(mode="json") for patch in update_fields],
3472
3989
  "remove_fields": [patch.model_dump(mode="json") for patch in remove_fields],
3473
3990
  }
3991
+ try:
3992
+ desired_auth = (
3993
+ self._compile_visibility_to_member_auth(profile=profile, visibility=visibility)
3994
+ if visibility is not None
3995
+ else None
3996
+ )
3997
+ except VisibilityResolutionError as error:
3998
+ return _failed(
3999
+ error.error_code,
4000
+ error.message,
4001
+ normalized_args=normalized_args,
4002
+ details=error.details,
4003
+ suggested_next_call=None,
4004
+ )
3474
4005
  permission_outcomes: list[PermissionCheckOutcome] = []
3475
4006
  requested_field_changes = bool(add_fields or update_fields or remove_fields)
3476
4007
  resolved: JSONObject
@@ -3517,6 +4048,7 @@ class AiBuilderFacade:
3517
4048
  package_tag_id=package_tag_id,
3518
4049
  icon=icon,
3519
4050
  color=color,
4051
+ auth=desired_auth,
3520
4052
  )
3521
4053
  if resolved.get("status") == "failed":
3522
4054
  if not isinstance(resolved.get("normalized_args"), dict) or not resolved.get("normalized_args"):
@@ -3534,7 +4066,7 @@ class AiBuilderFacade:
3534
4066
  requested_rename = requested_app_name if app_key else None
3535
4067
  requested_icon = str(icon or "").strip() or None
3536
4068
  requested_color = str(color or "").strip() or None
3537
- requested_app_base_update = bool(requested_rename or requested_icon or requested_color)
4069
+ requested_app_base_update = bool(requested_rename or requested_icon or requested_color or desired_auth is not None)
3538
4070
  effective_app_name = target.app_name
3539
4071
  if not bool(resolved.get("created")):
3540
4072
  permission_outcome = self._guard_app_permission(
@@ -3554,6 +4086,7 @@ class AiBuilderFacade:
3554
4086
  app_name=requested_rename,
3555
4087
  icon=requested_icon,
3556
4088
  color=requested_color,
4089
+ auth_override=desired_auth,
3557
4090
  normalized_args=normalized_args,
3558
4091
  )
3559
4092
  if visual_result.get("status") == "failed":
@@ -4898,6 +5431,20 @@ class AiBuilderFacade:
4898
5431
  created_key: str | None = None
4899
5432
  system_view_list_type = _resolve_system_view_list_type(view_key=existing_key, view_name=patch.name) if existing_key else None
4900
5433
  operation_phase = "view_update" if existing_key else "view_create"
5434
+ try:
5435
+ view_auth_override = (
5436
+ self._compile_visibility_to_member_auth(profile=profile, visibility=patch.visibility)
5437
+ if patch.visibility is not None
5438
+ else None
5439
+ )
5440
+ except VisibilityResolutionError as error:
5441
+ return _failed(
5442
+ error.error_code,
5443
+ error.message,
5444
+ normalized_args=normalized_args,
5445
+ details={"app_key": app_key, "view_name": patch.name, **error.details},
5446
+ suggested_next_call=None,
5447
+ )
4901
5448
  try:
4902
5449
  if existing_key:
4903
5450
  payload = _build_view_update_payload(
@@ -4908,6 +5455,7 @@ class AiBuilderFacade:
4908
5455
  patch=patch,
4909
5456
  view_filters=translated_filters,
4910
5457
  current_fields_by_name=current_fields_by_name,
5458
+ auth_override=view_auth_override,
4911
5459
  explicit_button_dtos=explicit_button_dtos,
4912
5460
  )
4913
5461
  self.views.view_update(profile=profile, viewgraph_key=existing_key, payload=payload)
@@ -4976,6 +5524,7 @@ class AiBuilderFacade:
4976
5524
  patch=patch,
4977
5525
  view_filters=translated_filters,
4978
5526
  current_fields_by_name=current_fields_by_name,
5527
+ auth_override=view_auth_override,
4979
5528
  explicit_button_dtos=explicit_button_dtos,
4980
5529
  )
4981
5530
  self.views.view_update(profile=profile, viewgraph_key=created_key, payload=payload)
@@ -4988,6 +5537,7 @@ class AiBuilderFacade:
4988
5537
  ordinal=ordinal,
4989
5538
  view_filters=translated_filters,
4990
5539
  current_fields_by_name=current_fields_by_name,
5540
+ auth_override=view_auth_override,
4991
5541
  explicit_button_dtos=explicit_button_dtos,
4992
5542
  )
4993
5543
  create_result = self.views.view_create(profile=profile, payload=payload)
@@ -5034,6 +5584,7 @@ class AiBuilderFacade:
5034
5584
  ordinal=ordinal,
5035
5585
  view_filters=translated_filters,
5036
5586
  current_fields_by_name=current_fields_by_name,
5587
+ auth_override=view_auth_override,
5037
5588
  explicit_button_dtos=fallback_button_dtos,
5038
5589
  )
5039
5590
  self.views.view_update(profile=profile, viewgraph_key=target_key, payload=fallback_payload)
@@ -5118,6 +5669,7 @@ class AiBuilderFacade:
5118
5669
  ordinal=ordinal,
5119
5670
  view_filters=translated_filters,
5120
5671
  current_fields_by_name=current_fields_by_name,
5672
+ auth_override=view_auth_override,
5121
5673
  explicit_button_dtos=explicit_button_dtos,
5122
5674
  )
5123
5675
  self.views.view_create(profile=profile, payload=fallback_payload)
@@ -5691,6 +6243,11 @@ class AiBuilderFacade:
5691
6243
 
5692
6244
  for patch in request.upsert_charts:
5693
6245
  try:
6246
+ chart_visible_auth = (
6247
+ self._compile_visibility_to_chart_visible_auth(profile=profile, visibility=patch.visibility)
6248
+ if patch.visibility is not None
6249
+ else None
6250
+ )
5694
6251
  existing = None
5695
6252
  if patch.chart_id:
5696
6253
  existing = existing_by_id.get(str(patch.chart_id))
@@ -5718,7 +6275,7 @@ class AiBuilderFacade:
5718
6275
  "tagId": "0",
5719
6276
  "parentId": "0",
5720
6277
  "dataSourceId": app_key,
5721
- "visibleAuth": qingbi_workspace_visible_auth(),
6278
+ "visibleAuth": deepcopy(chart_visible_auth if isinstance(chart_visible_auth, dict) else qingbi_workspace_visible_auth()),
5722
6279
  "editAuthList": [],
5723
6280
  "editAuthType": "ws",
5724
6281
  "editAuthIncludeSubDept": True,
@@ -5751,7 +6308,7 @@ class AiBuilderFacade:
5751
6308
  }
5752
6309
  existing_by_id[chart_id] = deepcopy(created_chart)
5753
6310
  existing_by_name.setdefault(patch.name, []).append(deepcopy(created_chart))
5754
- elif existing_name != patch.name or existing_type != target_type:
6311
+ elif existing_name != patch.name or existing_type != target_type or isinstance(chart_visible_auth, dict):
5755
6312
  base_info = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
5756
6313
  self.charts.qingbi_report_update_base(
5757
6314
  profile=profile,
@@ -5764,7 +6321,11 @@ class AiBuilderFacade:
5764
6321
  "tagId": "0",
5765
6322
  "parentId": "0",
5766
6323
  "dataSourceId": base_info.get("dataSourceId") or app_key,
5767
- "visibleAuth": deepcopy(base_info.get("visibleAuth") or qingbi_workspace_visible_auth()),
6324
+ "visibleAuth": deepcopy(
6325
+ chart_visible_auth
6326
+ if isinstance(chart_visible_auth, dict)
6327
+ else base_info.get("visibleAuth") or qingbi_workspace_visible_auth()
6328
+ ),
5768
6329
  "editAuthList": deepcopy(base_info.get("editAuthList") or []),
5769
6330
  "editAuthType": base_info.get("editAuthType") or "ws",
5770
6331
  "editAuthIncludeSubDept": bool(base_info.get("editAuthIncludeSubDept", True)),
@@ -5791,6 +6352,10 @@ class AiBuilderFacade:
5791
6352
  field_lookup=field_lookup,
5792
6353
  qingbi_fields_by_id=qingbi_fields_by_id,
5793
6354
  )
6355
+ if isinstance(chart_visible_auth, dict):
6356
+ raw_data_config = config_payload.get("rawDataConfigDTO")
6357
+ if isinstance(raw_data_config, dict):
6358
+ raw_data_config["authInfo"] = deepcopy(chart_visible_auth)
5794
6359
  self.charts.qingbi_report_update_config(profile=profile, chart_id=chart_id, payload=config_payload)
5795
6360
  if existing is not None and chart_id not in updated_ids:
5796
6361
  updated_ids.append(chart_id)
@@ -5816,7 +6381,7 @@ class AiBuilderFacade:
5816
6381
  "status": "created" if existing is None else "updated",
5817
6382
  }
5818
6383
  )
5819
- except (QingflowApiError, RuntimeError, ValueError) as error:
6384
+ except (QingflowApiError, RuntimeError, ValueError, VisibilityResolutionError) as error:
5820
6385
  api_error = _coerce_api_error(error) if not isinstance(error, ValueError) else None
5821
6386
  failure = {
5822
6387
  "chart_id": str(locals().get("chart_id") or patch.chart_id or ""),
@@ -5966,10 +6531,27 @@ class AiBuilderFacade:
5966
6531
  creating = not dash_key
5967
6532
  verify_dash_name = creating or request.dash_name is not None
5968
6533
  verify_dash_icon = bool(request.icon or request.color)
5969
- verify_auth = request.auth is not None
6534
+ verify_auth = request.visibility is not None or request.auth is not None
5970
6535
  verify_hide_copyright = request.hide_copyright is not None
5971
6536
  verify_dash_global_config = request.dash_global_config is not None
5972
6537
  verify_tags = creating or request.package_tag_id is not None
6538
+ requested_visibility = request.visibility
6539
+ if requested_visibility is None and isinstance(request.auth, dict) and request.auth:
6540
+ requested_visibility = VisibilityPatch.model_validate(_public_visibility_from_member_auth(request.auth))
6541
+ try:
6542
+ desired_auth = (
6543
+ self._compile_visibility_to_member_auth(profile=profile, visibility=requested_visibility)
6544
+ if requested_visibility is not None
6545
+ else None
6546
+ )
6547
+ except VisibilityResolutionError as error:
6548
+ return _failed(
6549
+ error.error_code,
6550
+ error.message,
6551
+ normalized_args=normalized_args,
6552
+ details=error.details,
6553
+ suggested_next_call=None,
6554
+ )
5973
6555
  try:
5974
6556
  base_payload = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") if dash_key else {}
5975
6557
  except (QingflowApiError, RuntimeError) as error:
@@ -6035,7 +6617,7 @@ class AiBuilderFacade:
6035
6617
  package_tag_id=target_package_tag_id,
6036
6618
  icon=request.icon,
6037
6619
  color=request.color,
6038
- auth=request.auth,
6620
+ auth=desired_auth,
6039
6621
  hide_copyright=request.hide_copyright,
6040
6622
  dash_global_config=request.dash_global_config,
6041
6623
  config=request.config,
@@ -6059,7 +6641,7 @@ class AiBuilderFacade:
6059
6641
  package_tag_id=target_package_tag_id,
6060
6642
  icon=request.icon,
6061
6643
  color=request.color,
6062
- auth=request.auth,
6644
+ auth=desired_auth,
6063
6645
  hide_copyright=request.hide_copyright,
6064
6646
  dash_global_config=request.dash_global_config,
6065
6647
  config=request.config,
@@ -6487,6 +7069,7 @@ class AiBuilderFacade:
6487
7069
  package_tag_id: int | None,
6488
7070
  icon: str | None,
6489
7071
  color: str | None,
7072
+ auth: dict[str, Any] | None = None,
6490
7073
  ) -> JSONObject:
6491
7074
  payload: JSONObject = {
6492
7075
  "appName": app_name or "未命名应用",
@@ -6496,7 +7079,7 @@ class AiBuilderFacade:
6496
7079
  title=app_name or "未命名应用",
6497
7080
  fallback_icon_name="template",
6498
7081
  ),
6499
- "auth": default_member_auth(),
7082
+ "auth": deepcopy(auth if isinstance(auth, dict) else default_member_auth()),
6500
7083
  "tagIds": [package_tag_id] if package_tag_id and package_tag_id > 0 else [],
6501
7084
  }
6502
7085
  try:
@@ -6564,8 +7147,9 @@ class AiBuilderFacade:
6564
7147
  raw_base: dict[str, Any],
6565
7148
  form_title: str,
6566
7149
  app_icon: str,
7150
+ auth_override: dict[str, Any] | None = None,
6567
7151
  ) -> JSONObject | None:
6568
- auth = deepcopy(raw_base.get("auth"))
7152
+ auth = deepcopy(auth_override if isinstance(auth_override, dict) else raw_base.get("auth"))
6569
7153
  if not isinstance(auth, dict):
6570
7154
  return None
6571
7155
  payload: JSONObject = {
@@ -6587,6 +7171,7 @@ class AiBuilderFacade:
6587
7171
  app_name: str | None,
6588
7172
  icon: str | None,
6589
7173
  color: str | None,
7174
+ auth_override: dict[str, Any] | None,
6590
7175
  normalized_args: dict[str, Any],
6591
7176
  ) -> JSONObject:
6592
7177
  try:
@@ -6604,6 +7189,7 @@ class AiBuilderFacade:
6604
7189
  effective_title = str(raw_base.get("formTitle") or fallback_title or "未命名应用").strip() or "未命名应用"
6605
7190
  desired_title = str(app_name or effective_title).strip() or effective_title
6606
7191
  existing_icon = str(raw_base.get("appIcon") or "").strip() or None
7192
+ existing_auth = deepcopy(raw_base.get("auth")) if isinstance(raw_base.get("auth"), dict) else None
6607
7193
  desired_icon = encode_workspace_icon_with_defaults(
6608
7194
  icon=icon,
6609
7195
  color=color,
@@ -6611,7 +7197,8 @@ class AiBuilderFacade:
6611
7197
  fallback_icon_name="template",
6612
7198
  existing_icon=existing_icon,
6613
7199
  )
6614
- if desired_icon == existing_icon and desired_title == effective_title:
7200
+ auth_changed = isinstance(auth_override, dict) and deepcopy(auth_override) != existing_auth
7201
+ if desired_icon == existing_icon and desired_title == effective_title and not auth_changed:
6615
7202
  return {
6616
7203
  "status": "success",
6617
7204
  "updated": False,
@@ -6620,7 +7207,12 @@ class AiBuilderFacade:
6620
7207
  "app_name_after": desired_title,
6621
7208
  "request_id": None,
6622
7209
  }
6623
- payload = self._build_app_base_update_payload(raw_base=raw_base, form_title=desired_title, app_icon=desired_icon)
7210
+ payload = self._build_app_base_update_payload(
7211
+ raw_base=raw_base,
7212
+ form_title=desired_title,
7213
+ app_icon=desired_icon,
7214
+ auth_override=auth_override,
7215
+ )
6624
7216
  if payload is None:
6625
7217
  return _failed(
6626
7218
  "APP_VISUAL_UPDATE_UNSUPPORTED",
@@ -7951,6 +8543,151 @@ def _verify_portal_readback(
7951
8543
  return not mismatches, mismatches
7952
8544
 
7953
8545
 
8546
+ def _empty_public_visibility_selectors(*, include_sub_departs: bool | None = None) -> dict[str, Any]:
8547
+ payload = {
8548
+ "member_uids": [],
8549
+ "member_emails": [],
8550
+ "member_names": [],
8551
+ "dept_ids": [],
8552
+ "dept_names": [],
8553
+ "role_ids": [],
8554
+ "role_names": [],
8555
+ }
8556
+ if include_sub_departs is not None:
8557
+ payload["include_sub_departs"] = bool(include_sub_departs)
8558
+ return payload
8559
+
8560
+
8561
+ def _empty_public_external_visibility_selectors() -> dict[str, Any]:
8562
+ return {
8563
+ "member_ids": [],
8564
+ "member_emails": [],
8565
+ "dept_ids": [],
8566
+ }
8567
+
8568
+
8569
+ def _public_visibility_from_member_auth(raw_auth: Any) -> dict[str, Any]:
8570
+ auth = raw_auth if isinstance(raw_auth, dict) and raw_auth else default_member_auth()
8571
+ root_type = str(auth.get("type") or "").strip().upper()
8572
+ contact_auth = auth.get("contactAuth") if isinstance(auth.get("contactAuth"), dict) else {}
8573
+ external_auth = auth.get("externalMemberAuth") if isinstance(auth.get("externalMemberAuth"), dict) else {}
8574
+ contact_members = contact_auth.get("authMembers") if isinstance(contact_auth.get("authMembers"), dict) else {}
8575
+ external_members = external_auth.get("authMembers") if isinstance(external_auth.get("authMembers"), dict) else {}
8576
+
8577
+ selectors = _empty_public_visibility_selectors(include_sub_departs=contact_members.get("includeSubDeparts"))
8578
+ for item in contact_members.get("member") or []:
8579
+ if not isinstance(item, dict):
8580
+ continue
8581
+ uid = _coerce_positive_int(item.get("uid") or item.get("id"))
8582
+ if uid is not None:
8583
+ selectors["member_uids"].append(uid)
8584
+ email = str(item.get("email") or "").strip()
8585
+ if email:
8586
+ selectors["member_emails"].append(email)
8587
+ name = str(item.get("nickName") or item.get("remark") or item.get("name") or "").strip()
8588
+ if name:
8589
+ selectors["member_names"].append(name)
8590
+ for item in contact_members.get("depart") or []:
8591
+ if not isinstance(item, dict):
8592
+ continue
8593
+ dept_id = _coerce_positive_int(item.get("deptId") or item.get("id"))
8594
+ if dept_id is not None:
8595
+ selectors["dept_ids"].append(dept_id)
8596
+ dept_name = str(item.get("deptName") or item.get("departName") or item.get("name") or "").strip()
8597
+ if dept_name:
8598
+ selectors["dept_names"].append(dept_name)
8599
+ for item in contact_members.get("role") or []:
8600
+ if not isinstance(item, dict):
8601
+ continue
8602
+ role_id = _coerce_positive_int(item.get("roleId") or item.get("id"))
8603
+ if role_id is not None:
8604
+ selectors["role_ids"].append(role_id)
8605
+ role_name = str(item.get("roleName") or item.get("name") or "").strip()
8606
+ if role_name:
8607
+ selectors["role_names"].append(role_name)
8608
+
8609
+ external_selectors = _empty_public_external_visibility_selectors()
8610
+ for item in external_members.get("member") or []:
8611
+ if not isinstance(item, dict):
8612
+ continue
8613
+ member_id = _coerce_positive_int(item.get("uid") or item.get("memberId") or item.get("id"))
8614
+ if member_id is not None:
8615
+ external_selectors["member_ids"].append(member_id)
8616
+ member_email = str(item.get("email") or "").strip()
8617
+ if member_email:
8618
+ external_selectors["member_emails"].append(member_email)
8619
+ for item in external_members.get("depart") or []:
8620
+ if not isinstance(item, dict):
8621
+ continue
8622
+ dept_id = _coerce_positive_int(item.get("deptId") or item.get("id"))
8623
+ if dept_id is not None:
8624
+ external_selectors["dept_ids"].append(dept_id)
8625
+
8626
+ if root_type == "ALL":
8627
+ mode = PublicVisibilityMode.everyone.value
8628
+ external_mode = PublicExternalVisibilityMode.workspace.value
8629
+ else:
8630
+ contact_type = str(contact_auth.get("type") or "").strip().upper()
8631
+ external_type = str(external_auth.get("type") or "").strip().upper()
8632
+ mode = PublicVisibilityMode.specific.value if contact_type == "SPECIFIC" else PublicVisibilityMode.workspace.value
8633
+ external_mode = {
8634
+ "WORKSPACE_ALL": PublicExternalVisibilityMode.workspace.value,
8635
+ "SPECIFIC": PublicExternalVisibilityMode.specific.value,
8636
+ }.get(external_type, PublicExternalVisibilityMode.not_.value)
8637
+
8638
+ return {
8639
+ "mode": mode,
8640
+ "selectors": selectors,
8641
+ "external_mode": external_mode,
8642
+ "external_selectors": external_selectors,
8643
+ }
8644
+
8645
+
8646
+ def _public_visibility_from_chart_visible_auth(raw_auth: Any) -> dict[str, Any]:
8647
+ auth = raw_auth if isinstance(raw_auth, dict) and raw_auth else qingbi_workspace_visible_auth()
8648
+ root_type = str(auth.get("type") or "").strip().lower()
8649
+ contact_auth = auth.get("contactAuth") if isinstance(auth.get("contactAuth"), dict) else {}
8650
+ external_auth = auth.get("externalMemberAuth") if isinstance(auth.get("externalMemberAuth"), dict) else {}
8651
+ contact_members = contact_auth.get("authMembers") if isinstance(contact_auth.get("authMembers"), dict) else {}
8652
+ external_members = external_auth.get("authMembers") if isinstance(external_auth.get("authMembers"), dict) else {}
8653
+ member_auth = {
8654
+ "type": "ALL" if root_type == "all" else "WORKSPACE",
8655
+ "contactAuth": {
8656
+ "type": "SPECIFIC" if str(contact_auth.get("type") or "").strip().lower() == "assign" else "WORKSPACE_ALL",
8657
+ "authMembers": {
8658
+ "member": deepcopy(contact_members.get("member") or []),
8659
+ "depart": deepcopy(contact_members.get("depart") or []),
8660
+ "role": deepcopy(contact_members.get("role") or []),
8661
+ "dynamic": [],
8662
+ "includeSubDeparts": contact_members.get("includeSubDeparts"),
8663
+ },
8664
+ },
8665
+ "externalMemberAuth": {
8666
+ "type": {
8667
+ "assign": "SPECIFIC",
8668
+ "all": "WORKSPACE_ALL",
8669
+ }.get(str(external_auth.get("type") or "").strip().lower(), "NOT"),
8670
+ "authMembers": {
8671
+ "member": deepcopy(external_members.get("member") or []),
8672
+ "depart": deepcopy(external_members.get("depart") or []),
8673
+ "role": [],
8674
+ "dynamic": [],
8675
+ "includeSubDeparts": None,
8676
+ },
8677
+ },
8678
+ }
8679
+ return _public_visibility_from_member_auth(member_auth)
8680
+
8681
+
8682
+ def _visibility_summary(visibility: dict[str, Any]) -> dict[str, Any]:
8683
+ if not isinstance(visibility, dict):
8684
+ return {}
8685
+ return {
8686
+ "mode": str(visibility.get("mode") or PublicVisibilityMode.workspace.value),
8687
+ "external_mode": str(visibility.get("external_mode") or PublicExternalVisibilityMode.not_.value),
8688
+ }
8689
+
8690
+
7954
8691
  def _mapping_contains(actual: Any, expected: Any) -> bool:
7955
8692
  if isinstance(expected, dict):
7956
8693
  if not isinstance(actual, dict):
@@ -11538,6 +12275,8 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
11538
12275
  summary = deepcopy(base)
11539
12276
  if not isinstance(config, dict) or not config:
11540
12277
  return summary
12278
+ if isinstance(config.get("auth"), dict):
12279
+ summary["visibility_summary"] = _visibility_summary(_public_visibility_from_member_auth(config.get("auth")))
11541
12280
  legacy_columns = [str(value) for value in (summary.get("columns") or []) if str(value or "").strip()]
11542
12281
  question_entries = _extract_view_question_entries(config.get("viewgraphQuestions"))
11543
12282
  question_entries_by_id = {
@@ -12637,6 +13376,7 @@ def _build_view_create_payload(
12637
13376
  ordinal: int,
12638
13377
  view_filters: list[list[dict[str, Any]]],
12639
13378
  current_fields_by_name: dict[str, dict[str, Any]],
13379
+ auth_override: dict[str, Any] | None = None,
12640
13380
  explicit_button_dtos: list[dict[str, Any]] | None = None,
12641
13381
  ) -> JSONObject:
12642
13382
  entity = _entity_spec_from_app(base_info=base_info, schema=schema, views=None)
@@ -12663,7 +13403,7 @@ def _build_view_create_payload(
12663
13403
  payload["ordinal"] = ordinal
12664
13404
  payload["viewgraphQueIds"] = visible_que_ids
12665
13405
  payload["viewgraphQuestions"] = _build_viewgraph_questions(schema, visible_que_ids)
12666
- payload["auth"] = default_member_auth()
13406
+ payload["auth"] = deepcopy(auth_override if isinstance(auth_override, dict) else default_member_auth())
12667
13407
  payload.setdefault("sortType", "defaultSort")
12668
13408
  payload.setdefault("viewgraphSorts", [{"queId": 0, "beingSortAscend": True, "queType": 8}])
12669
13409
  if patch.type.value in {"card", "board", "gantt"}:
@@ -12829,6 +13569,7 @@ def _build_view_update_payload(
12829
13569
  patch: ViewUpsertPatch,
12830
13570
  view_filters: list[list[dict[str, Any]]],
12831
13571
  current_fields_by_name: dict[str, dict[str, Any]],
13572
+ auth_override: dict[str, Any] | None = None,
12832
13573
  explicit_button_dtos: list[dict[str, Any]] | None = None,
12833
13574
  ) -> JSONObject:
12834
13575
  config_response = views.view_get_config(profile=profile, viewgraph_key=source_viewgraph_key)
@@ -12857,6 +13598,7 @@ def _build_view_update_payload(
12857
13598
  payload.pop("id", None)
12858
13599
 
12859
13600
  payload["viewgraphName"] = patch.name
13601
+ payload["auth"] = deepcopy(auth_override if isinstance(auth_override, dict) else payload.get("auth") or default_member_auth())
12860
13602
  if "viewName" in payload:
12861
13603
  payload["viewName"] = patch.name
12862
13604
  payload["viewgraphQueIds"] = visible_que_id_values
@@ -12931,6 +13673,7 @@ def _build_minimal_view_payload(
12931
13673
  ordinal: int,
12932
13674
  view_filters: list[list[dict[str, Any]]],
12933
13675
  current_fields_by_name: dict[str, dict[str, Any]],
13676
+ auth_override: dict[str, Any] | None = None,
12934
13677
  explicit_button_dtos: list[dict[str, Any]] | None = None,
12935
13678
  ) -> JSONObject:
12936
13679
  field_map = extract_field_map(schema)
@@ -12950,7 +13693,7 @@ def _build_minimal_view_payload(
12950
13693
  "ordinal": ordinal,
12951
13694
  "viewgraphQueIds": visible_que_id_values,
12952
13695
  "viewgraphQuestions": _build_viewgraph_questions(schema, visible_que_id_values),
12953
- "auth": default_member_auth(),
13696
+ "auth": deepcopy(auth_override if isinstance(auth_override, dict) else default_member_auth()),
12954
13697
  }
12955
13698
  if patch.type.value in {"card", "board", "gantt"}:
12956
13699
  payload["beingShowTitleQue"] = True