feat: add debugMode to config, filter bot own captions to prevent repeats
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b616688411
commit
7778325e5e
6 changed files with 283 additions and 148 deletions
|
|
@ -39,6 +39,7 @@ class BrowserBotConnector:
|
|||
botAccountEmail: Optional[str] = None,
|
||||
botAccountPassword: Optional[str] = None,
|
||||
transferMode: str = "auto",
|
||||
debugMode: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Send join command to the Browser Bot service.
|
||||
|
|
@ -75,6 +76,7 @@ class BrowserBotConnector:
|
|||
"gatewayWsUrl": gatewayWsUrl,
|
||||
"language": language,
|
||||
"transferMode": transferMode,
|
||||
"debugMode": debugMode,
|
||||
}
|
||||
|
||||
# Add authenticated join credentials if configured
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ class TeamsbotConfig(BaseModel):
|
|||
triggerIntervalSeconds: int = Field(default=10, ge=3, le=60, description="Seconds between periodic AI analysis triggers")
|
||||
triggerCooldownSeconds: int = Field(default=3, ge=1, le=30, description="Minimum seconds between AI calls")
|
||||
contextWindowSegments: int = Field(default=20, ge=5, le=100, description="Number of transcript segments to include in AI context")
|
||||
debugMode: bool = Field(default=False, description="Enable debug mode: screenshots at every join step for diagnostics")
|
||||
|
||||
def _getEffectiveBrowserBotUrl(self) -> Optional[str]:
|
||||
"""Resolve the effective browser bot URL: per-instance config takes priority, then env variable."""
|
||||
|
|
@ -228,6 +229,7 @@ class TeamsbotConfigUpdateRequest(BaseModel):
|
|||
triggerIntervalSeconds: Optional[int] = None
|
||||
triggerCooldownSeconds: Optional[int] = None
|
||||
contextWindowSegments: Optional[int] = None
|
||||
debugMode: Optional[bool] = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ class TeamsbotService:
|
|||
botAccountEmail=botAccountEmail,
|
||||
botAccountPassword=botAccountPassword,
|
||||
transferMode=self.config.transferMode if hasattr(self.config, 'transferMode') else "auto",
|
||||
debugMode=self.config.debugMode if hasattr(self.config, 'debugMode') else False,
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
|
|
@ -458,8 +459,13 @@ class TeamsbotService:
|
|||
if not text:
|
||||
return
|
||||
|
||||
# Filter out the bot's own speech from AI triggering.
|
||||
# Filter out the bot's own speech entirely — captions of the bot's
|
||||
# own voice come back as garbled text (e.g. German TTS → English caption)
|
||||
# which pollutes the context buffer and confuses AI analysis.
|
||||
isBotSpeaker = self._isBotSpeaker(speaker)
|
||||
if isBotSpeaker:
|
||||
logger.debug(f"Session {sessionId}: Ignoring own bot caption from: [{speaker}] {text[:80]}...")
|
||||
return
|
||||
|
||||
# Differential transcript writing:
|
||||
# If the same speaker is still talking and the new text is a
|
||||
|
|
@ -538,11 +544,6 @@ class TeamsbotService:
|
|||
"isContinuation": isContinuation,
|
||||
})
|
||||
|
||||
# Skip AI analysis for bot's own speech (prevents feedback loop)
|
||||
if isBotSpeaker:
|
||||
logger.debug(f"Session {sessionId}: Skipping AI trigger for bot's own speech: [{speaker}] {text[:60]}...")
|
||||
return
|
||||
|
||||
# Check if AI analysis should be triggered (only for final transcripts)
|
||||
if not isFinal:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -531,6 +531,34 @@ def getEffectivePermissions(
|
|||
try:
|
||||
interface = getRootInterface()
|
||||
|
||||
# MandateAdmin: verify the requested user shares at least one admin mandate
|
||||
if not context.hasSysAdminRole:
|
||||
adminMandateIds = []
|
||||
adminUserMandates = interface.getUserMandates(str(context.user.id))
|
||||
for um in adminUserMandates:
|
||||
umId = getattr(um, 'id', None)
|
||||
mid = getattr(um, 'mandateId', None)
|
||||
if not umId or not mid:
|
||||
continue
|
||||
roleIds = interface.getRoleIdsForUserMandate(str(umId))
|
||||
for roleId in roleIds:
|
||||
role = interface.getRole(roleId)
|
||||
if role and role.roleLabel == "admin" and not role.featureInstanceId:
|
||||
adminMandateIds.append(str(mid))
|
||||
break
|
||||
|
||||
if not adminMandateIds:
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
userInAdminMandate = False
|
||||
for mid in adminMandateIds:
|
||||
if _isUserInMandate(interface, userId, mid):
|
||||
userInAdminMandate = True
|
||||
break
|
||||
|
||||
if not userInAdminMandate:
|
||||
raise HTTPException(status_code=403, detail="Benutzer gehört nicht zu Ihrem Mandate")
|
||||
|
||||
# Get user
|
||||
user = interface.getUser(userId)
|
||||
if not user:
|
||||
|
|
|
|||
|
|
@ -336,6 +336,33 @@ def _getInstanceViewPermissions(
|
|||
return permissions # Fail-safe: no permissions on error
|
||||
|
||||
|
||||
def _filterItems(
|
||||
items: List[Dict[str, Any]],
|
||||
language: str,
|
||||
isSysAdmin: bool,
|
||||
roleIds: List[str],
|
||||
hasGlobalPermission: bool
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Filter and format navigation items based on permissions."""
|
||||
filteredItems = []
|
||||
for item in items:
|
||||
if item.get("adminOnly") and not isSysAdmin:
|
||||
if not hasGlobalPermission and not _checkUiPermission(roleIds, item["objectKey"]):
|
||||
continue
|
||||
if item.get("sysAdminOnly") and not isSysAdmin:
|
||||
continue
|
||||
if item.get("public"):
|
||||
filteredItems.append(_formatBlockItem(item, language))
|
||||
continue
|
||||
if isSysAdmin:
|
||||
filteredItems.append(_formatBlockItem(item, language))
|
||||
continue
|
||||
if hasGlobalPermission or _checkUiPermission(roleIds, item["objectKey"]):
|
||||
filteredItems.append(_formatBlockItem(item, language))
|
||||
filteredItems.sort(key=lambda i: i["order"])
|
||||
return filteredItems
|
||||
|
||||
|
||||
def _buildStaticBlocks(
|
||||
language: str,
|
||||
isSysAdmin: bool,
|
||||
|
|
@ -346,40 +373,54 @@ def _buildStaticBlocks(
|
|||
Build static navigation blocks from NAVIGATION_SECTIONS.
|
||||
|
||||
Returns list of blocks with items filtered by permissions.
|
||||
Supports subgroups within sections.
|
||||
"""
|
||||
blocks = []
|
||||
|
||||
for section in NAVIGATION_SECTIONS:
|
||||
# Filter items based on UI AccessRules (ui.admin.*, ui.billing.*, etc.)
|
||||
filteredItems = []
|
||||
for item in section.get("items", []):
|
||||
# Public items are always visible
|
||||
if item.get("public"):
|
||||
filteredItems.append(_formatBlockItem(item, language))
|
||||
continue
|
||||
|
||||
# SysAdmin-only items (e.g. automation-events) require isSysAdmin
|
||||
if item.get("sysAdminOnly") and not isSysAdmin:
|
||||
continue
|
||||
|
||||
# Check UI AccessRule for this objectKey
|
||||
# Roles with item=None rule (e.g. sysadmin) get access to everything
|
||||
# Roles with specific ui.admin.* rules get access to those items
|
||||
if hasGlobalPermission or _checkUiPermission(roleIds, item["objectKey"]):
|
||||
filteredItems.append(_formatBlockItem(item, language))
|
||||
if section.get("adminOnly") and not isSysAdmin:
|
||||
continue
|
||||
|
||||
# Only include section if it has visible items
|
||||
if filteredItems:
|
||||
# Sort items by order
|
||||
filteredItems.sort(key=lambda i: i["order"])
|
||||
# Handle sections with subgroups
|
||||
if "subgroups" in section:
|
||||
filteredSubgroups = []
|
||||
for subgroup in section["subgroups"]:
|
||||
subItems = _filterItems(
|
||||
subgroup.get("items", []), language, isSysAdmin, roleIds, hasGlobalPermission
|
||||
)
|
||||
if subItems:
|
||||
filteredSubgroups.append({
|
||||
"id": subgroup["id"],
|
||||
"title": subgroup["title"].get(language, subgroup["title"].get("en", subgroup["id"])),
|
||||
"order": subgroup.get("order", 50),
|
||||
"items": subItems,
|
||||
})
|
||||
|
||||
blocks.append({
|
||||
"type": "static",
|
||||
"id": section["id"],
|
||||
"title": section["title"].get(language, section["title"].get("en", section["id"])),
|
||||
"order": section.get("order", 50),
|
||||
"items": filteredItems,
|
||||
})
|
||||
filteredSubgroups.sort(key=lambda s: s["order"])
|
||||
|
||||
if filteredSubgroups:
|
||||
blocks.append({
|
||||
"type": "static",
|
||||
"id": section["id"],
|
||||
"title": section["title"].get(language, section["title"].get("en", section["id"])),
|
||||
"order": section.get("order", 50),
|
||||
"items": [],
|
||||
"subgroups": filteredSubgroups,
|
||||
})
|
||||
else:
|
||||
# Standard flat section
|
||||
filteredItems = _filterItems(
|
||||
section.get("items", []), language, isSysAdmin, roleIds, hasGlobalPermission
|
||||
)
|
||||
|
||||
if filteredItems:
|
||||
blocks.append({
|
||||
"type": "static",
|
||||
"id": section["id"],
|
||||
"title": section["title"].get(language, section["title"].get("en", section["id"])),
|
||||
"order": section.get("order", 50),
|
||||
"items": filteredItems,
|
||||
})
|
||||
|
||||
return blocks
|
||||
|
||||
|
|
|
|||
|
|
@ -106,132 +106,176 @@ NAVIGATION_SECTIONS = [
|
|||
},
|
||||
],
|
||||
},
|
||||
# ─── Administration (with subgroups) ───
|
||||
# Access control is at item level, NOT section level.
|
||||
# Groups auto-hide if 0 visible pages for the user.
|
||||
{
|
||||
"id": "admin",
|
||||
"title": {"en": "ADMINISTRATION", "de": "ADMINISTRATION", "fr": "ADMINISTRATION"},
|
||||
"order": 200,
|
||||
"adminOnly": True,
|
||||
"items": [
|
||||
"subgroups": [
|
||||
# ── Wizards ──
|
||||
{
|
||||
"id": "admin-users",
|
||||
"objectKey": "ui.admin.users",
|
||||
"label": {"en": "Users", "de": "Benutzer", "fr": "Utilisateurs"},
|
||||
"icon": "FaUsers",
|
||||
"path": "/admin/users",
|
||||
"id": "admin-wizards",
|
||||
"title": {"en": "Wizards", "de": "Wizards", "fr": "Assistants"},
|
||||
"order": 10,
|
||||
"adminOnly": True,
|
||||
"items": [
|
||||
{
|
||||
"id": "admin-mandate-wizard",
|
||||
"objectKey": "ui.admin.mandateWizard",
|
||||
"label": {"en": "Mandate Wizard", "de": "Mandanten-Wizard", "fr": "Assistant mandat"},
|
||||
"icon": "FaMagic",
|
||||
"path": "/admin/mandate-wizard",
|
||||
"order": 10,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-invitation-wizard",
|
||||
"objectKey": "ui.admin.invitationWizard",
|
||||
"label": {"en": "Invitation Wizard", "de": "Einladungs-Wizard", "fr": "Assistant d'invitation"},
|
||||
"icon": "FaEnvelopeOpenText",
|
||||
"path": "/admin/invitation-wizard",
|
||||
"order": 20,
|
||||
"adminOnly": True,
|
||||
},
|
||||
],
|
||||
},
|
||||
# ── Users ──
|
||||
{
|
||||
"id": "admin-invitations",
|
||||
"objectKey": "ui.admin.invitations",
|
||||
"label": {"en": "User Invitations", "de": "Benutzer-Einladungen", "fr": "Invitations utilisateurs"},
|
||||
"icon": "FaEnvelopeOpenText",
|
||||
"path": "/admin/invitations",
|
||||
"order": 12,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-user-access-overview",
|
||||
"objectKey": "ui.admin.userAccessOverview",
|
||||
"label": {"en": "User Access Overview", "de": "Benutzer-Zugriffsübersicht", "fr": "Aperçu des accès utilisateur"},
|
||||
"icon": "FaClipboardList",
|
||||
"path": "/admin/user-access-overview",
|
||||
"order": 14,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-mandates",
|
||||
"objectKey": "ui.admin.mandates",
|
||||
"label": {"en": "Mandates", "de": "Mandanten", "fr": "Mandats"},
|
||||
"icon": "FaBuilding",
|
||||
"path": "/admin/mandates",
|
||||
"id": "admin-users-group",
|
||||
"title": {"en": "Users", "de": "Benutzer", "fr": "Utilisateurs"},
|
||||
"order": 20,
|
||||
"adminOnly": True,
|
||||
"items": [
|
||||
{
|
||||
"id": "admin-users",
|
||||
"objectKey": "ui.admin.users",
|
||||
"label": {"en": "Users", "de": "Benutzer", "fr": "Utilisateurs"},
|
||||
"icon": "FaUsers",
|
||||
"path": "/admin/users",
|
||||
"order": 10,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-invitations",
|
||||
"objectKey": "ui.admin.invitations",
|
||||
"label": {"en": "User Invitations", "de": "Benutzer-Einladungen", "fr": "Invitations utilisateurs"},
|
||||
"icon": "FaEnvelopeOpenText",
|
||||
"path": "/admin/invitations",
|
||||
"order": 20,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-user-access-overview",
|
||||
"objectKey": "ui.admin.userAccessOverview",
|
||||
"label": {"en": "User Access Overview", "de": "Benutzer-Zugriffsübersicht", "fr": "Aperçu des accès utilisateur"},
|
||||
"icon": "FaClipboardList",
|
||||
"path": "/admin/user-access-overview",
|
||||
"order": 30,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-billing",
|
||||
"objectKey": "ui.admin.billing",
|
||||
"label": {"en": "Billing Administration", "de": "Billing-Verwaltung", "fr": "Administration de facturation"},
|
||||
"icon": "FaMoneyBillAlt",
|
||||
"path": "/admin/billing",
|
||||
"order": 40,
|
||||
"adminOnly": True,
|
||||
"sysAdminOnly": True,
|
||||
},
|
||||
],
|
||||
},
|
||||
# ── System ──
|
||||
{
|
||||
"id": "admin-user-mandates",
|
||||
"objectKey": "ui.admin.userMandates",
|
||||
"label": {"en": "Mandate Members", "de": "Mandanten-Mitglieder", "fr": "Membres du mandat"},
|
||||
"icon": "FaUserFriends",
|
||||
"path": "/admin/user-mandates",
|
||||
"order": 25,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-access",
|
||||
"objectKey": "ui.admin.access",
|
||||
"label": {"en": "Access Management", "de": "Zugriffsverwaltung", "fr": "Gestion des accès"},
|
||||
"icon": "FaBuilding",
|
||||
"path": "/admin/access",
|
||||
"id": "admin-system-group",
|
||||
"title": {"en": "System", "de": "System", "fr": "Système"},
|
||||
"order": 30,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-roles",
|
||||
"objectKey": "ui.admin.roles",
|
||||
"label": {"en": "Roles", "de": "Rollen", "fr": "Rôles"},
|
||||
"icon": "FaUserTag",
|
||||
"path": "/admin/mandate-roles",
|
||||
"order": 40,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-mandate-role-permissions",
|
||||
"objectKey": "ui.admin.mandateRolePermissions",
|
||||
"label": {"en": "Role Permissions", "de": "Rollen-Berechtigungen", "fr": "Permissions des rôles"},
|
||||
"icon": "FaKey",
|
||||
"path": "/admin/mandate-role-permissions",
|
||||
"order": 45,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-feature-instances",
|
||||
"objectKey": "ui.admin.featureInstances",
|
||||
"label": {"en": "Feature Instances", "de": "Feature-Instanzen", "fr": "Instances de features"},
|
||||
"icon": "FaCubes",
|
||||
"path": "/admin/feature-instances",
|
||||
"order": 48,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-feature-roles",
|
||||
"objectKey": "ui.admin.featureRoles",
|
||||
"label": {"en": "Feature Role Templates", "de": "Features Rollen-Vorlagen", "fr": "Modèles de rôles features"},
|
||||
"icon": "FaShieldAlt",
|
||||
"path": "/admin/feature-roles",
|
||||
"order": 50,
|
||||
"adminOnly": True,
|
||||
"sysAdminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-billing",
|
||||
"objectKey": "ui.admin.billing",
|
||||
"label": {"en": "Billing Administration", "de": "Billing-Verwaltung", "fr": "Administration de facturation"},
|
||||
"icon": "FaMoneyBillAlt",
|
||||
"path": "/admin/billing",
|
||||
"order": 60,
|
||||
"adminOnly": True,
|
||||
"sysAdminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-automation-events",
|
||||
"objectKey": "ui.admin.automationEvents",
|
||||
"label": {"en": "Automation Events", "de": "Automation Events", "fr": "Événements d'automatisation"},
|
||||
"icon": "FaClock",
|
||||
"path": "/admin/automation-events",
|
||||
"order": 65,
|
||||
"adminOnly": True,
|
||||
"sysAdminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-logs",
|
||||
"objectKey": "ui.admin.logs",
|
||||
"label": {"en": "Logs", "de": "Logs", "fr": "Logs"},
|
||||
"icon": "FaFileAlt",
|
||||
"path": "/admin/logs",
|
||||
"order": 70,
|
||||
"adminOnly": True,
|
||||
"sysAdminOnly": True,
|
||||
"items": [
|
||||
{
|
||||
"id": "admin-roles",
|
||||
"objectKey": "ui.admin.roles",
|
||||
"label": {"en": "Roles", "de": "Rollen", "fr": "Rôles"},
|
||||
"icon": "FaUserTag",
|
||||
"path": "/admin/mandate-roles",
|
||||
"order": 10,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-mandate-role-permissions",
|
||||
"objectKey": "ui.admin.mandateRolePermissions",
|
||||
"label": {"en": "Role Permissions", "de": "Rollen-Berechtigungen", "fr": "Permissions des rôles"},
|
||||
"icon": "FaKey",
|
||||
"path": "/admin/mandate-role-permissions",
|
||||
"order": 20,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-mandates",
|
||||
"objectKey": "ui.admin.mandates",
|
||||
"label": {"en": "Mandates", "de": "Mandanten", "fr": "Mandats"},
|
||||
"icon": "FaBuilding",
|
||||
"path": "/admin/mandates",
|
||||
"order": 30,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-user-mandates",
|
||||
"objectKey": "ui.admin.userMandates",
|
||||
"label": {"en": "Mandate Members", "de": "Mandanten-Mitglieder", "fr": "Membres du mandat"},
|
||||
"icon": "FaUserFriends",
|
||||
"path": "/admin/user-mandates",
|
||||
"order": 40,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-access",
|
||||
"objectKey": "ui.admin.access",
|
||||
"label": {"en": "Access Management", "de": "Zugriffsverwaltung", "fr": "Gestion des accès"},
|
||||
"icon": "FaBuilding",
|
||||
"path": "/admin/access",
|
||||
"order": 50,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-feature-instances",
|
||||
"objectKey": "ui.admin.featureInstances",
|
||||
"label": {"en": "Feature Instances", "de": "Feature-Instanzen", "fr": "Instances de features"},
|
||||
"icon": "FaCubes",
|
||||
"path": "/admin/feature-instances",
|
||||
"order": 60,
|
||||
"adminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-feature-roles",
|
||||
"objectKey": "ui.admin.featureRoles",
|
||||
"label": {"en": "Feature Role Templates", "de": "Features Rollen-Vorlagen", "fr": "Modèles de rôles features"},
|
||||
"icon": "FaShieldAlt",
|
||||
"path": "/admin/feature-roles",
|
||||
"order": 70,
|
||||
"adminOnly": True,
|
||||
"sysAdminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-automation-events",
|
||||
"objectKey": "ui.admin.automationEvents",
|
||||
"label": {"en": "Automation Events", "de": "Automation Events", "fr": "Événements d'automatisation"},
|
||||
"icon": "FaClock",
|
||||
"path": "/admin/automation-events",
|
||||
"order": 80,
|
||||
"adminOnly": True,
|
||||
"sysAdminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-logs",
|
||||
"objectKey": "ui.admin.logs",
|
||||
"label": {"en": "Logs", "de": "Logs", "fr": "Logs"},
|
||||
"icon": "FaFileAlt",
|
||||
"path": "/admin/logs",
|
||||
"order": 90,
|
||||
"adminOnly": True,
|
||||
"sysAdminOnly": True,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -255,6 +299,7 @@ def _buildUiObjectsFromNavigation() -> List[Dict[str, Any]]:
|
|||
"""Build UI_OBJECTS list from NAVIGATION_SECTIONS for RBAC registration."""
|
||||
uiObjects = []
|
||||
for section in NAVIGATION_SECTIONS:
|
||||
# Process direct items
|
||||
for item in section.get("items", []):
|
||||
uiObjects.append({
|
||||
"objectKey": item["objectKey"],
|
||||
|
|
@ -268,6 +313,22 @@ def _buildUiObjectsFromNavigation() -> List[Dict[str, Any]]:
|
|||
"icon": item["icon"],
|
||||
}
|
||||
})
|
||||
# Process subgroups (nested items within section)
|
||||
for subgroup in section.get("subgroups", []):
|
||||
for item in subgroup.get("items", []):
|
||||
uiObjects.append({
|
||||
"objectKey": item["objectKey"],
|
||||
"label": item["label"],
|
||||
"meta": {
|
||||
"area": section["id"],
|
||||
"subgroup": subgroup["id"],
|
||||
"public": item.get("public", False),
|
||||
"adminOnly": item.get("adminOnly", False),
|
||||
"deprecated": item.get("deprecated", False),
|
||||
"path": item["path"],
|
||||
"icon": item["icon"],
|
||||
}
|
||||
})
|
||||
return uiObjects
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue