@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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +109 -0
- package/src/qingflow_mcp/builder_facade/service.py +761 -18
- package/src/qingflow_mcp/cli/commands/builder.py +33 -0
- package/src/qingflow_mcp/public_surface.py +2 -0
- package/src/qingflow_mcp/server_app_builder.py +28 -1
- package/src/qingflow_mcp/tools/ai_builder_tools.py +280 -19
|
@@ -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":
|
|
2676
|
-
|
|
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(
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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(
|
|
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
|