diff --git a/backend/core/config_manager.py b/backend/core/config_manager.py index 8c3367a..35819ba 100644 --- a/backend/core/config_manager.py +++ b/backend/core/config_manager.py @@ -95,7 +95,7 @@ class BotConfigManager: "token": secret, "proxy": extra.get("proxy", ""), "replyToMessage": bool(extra.get("replyToMessage", False)), - "allowFrom": extra.get("allowFrom", []), + "allowFrom": self._normalize_allow_from(extra.get("allowFrom", [])), } continue @@ -106,7 +106,7 @@ class BotConfigManager: "appSecret": secret, "encryptKey": extra.get("encryptKey", ""), "verificationToken": extra.get("verificationToken", ""), - "allowFrom": extra.get("allowFrom", []), + "allowFrom": self._normalize_allow_from(extra.get("allowFrom", [])), } continue @@ -115,7 +115,7 @@ class BotConfigManager: "enabled": enabled, "clientId": external, "clientSecret": secret, - "allowFrom": extra.get("allowFrom", []), + "allowFrom": self._normalize_allow_from(extra.get("allowFrom", [])), } continue @@ -137,7 +137,7 @@ class BotConfigManager: "enabled": enabled, "appId": external, "secret": secret, - "allowFrom": extra.get("allowFrom", []), + "allowFrom": self._normalize_allow_from(extra.get("allowFrom", [])), } continue @@ -167,3 +167,15 @@ class BotConfigManager: f.write(str(content).strip() + "\n") return dot_nanobot_dir + + @staticmethod + def _normalize_allow_from(raw: Any) -> List[str]: + rows: List[str] = [] + if isinstance(raw, list): + for item in raw: + text = str(item or "").strip() + if text and text not in rows: + rows.append(text) + if not rows: + return ["*"] + return rows diff --git a/backend/main.py b/backend/main.py index 1552b9c..bfaa233 100644 --- a/backend/main.py +++ b/backend/main.py @@ -615,6 +615,18 @@ def _normalize_channel_extra(raw: Any) -> Dict[str, Any]: return raw +def _normalize_allow_from(raw: Any) -> List[str]: + rows: List[str] = [] + if isinstance(raw, list): + for item in raw: + text = str(item or "").strip() + if text and text not in rows: + rows.append(text) + if not rows: + return ["*"] + return rows + + def _read_global_delivery_flags(channels_cfg: Any) -> tuple[bool, bool]: if not isinstance(channels_cfg, dict): return False, False @@ -643,18 +655,18 @@ def _channel_cfg_to_api_dict(bot_id: str, ctype: str, cfg: Dict[str, Any]) -> Di extra = { "encryptKey": cfg.get("encryptKey", ""), "verificationToken": cfg.get("verificationToken", ""), - "allowFrom": cfg.get("allowFrom", []), + "allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])), } elif ctype == "dingtalk": external_app_id = str(cfg.get("clientId") or "") app_secret = str(cfg.get("clientSecret") or "") - extra = {"allowFrom": cfg.get("allowFrom", [])} + extra = {"allowFrom": _normalize_allow_from(cfg.get("allowFrom", []))} elif ctype == "telegram": app_secret = str(cfg.get("token") or "") extra = { "proxy": cfg.get("proxy", ""), "replyToMessage": bool(cfg.get("replyToMessage", False)), - "allowFrom": cfg.get("allowFrom", []), + "allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])), } elif ctype == "slack": external_app_id = str(cfg.get("botToken") or "") @@ -669,7 +681,7 @@ def _channel_cfg_to_api_dict(bot_id: str, ctype: str, cfg: Dict[str, Any]) -> Di elif ctype == "qq": external_app_id = str(cfg.get("appId") or "") app_secret = str(cfg.get("secret") or "") - extra = {"allowFrom": cfg.get("allowFrom", [])} + extra = {"allowFrom": _normalize_allow_from(cfg.get("allowFrom", []))} else: external_app_id = str( cfg.get("appId") or cfg.get("clientId") or cfg.get("botToken") or cfg.get("externalAppId") or "" @@ -707,14 +719,14 @@ def _channel_api_to_cfg(row: Dict[str, Any]) -> Dict[str, Any]: "appSecret": app_secret, "encryptKey": extra.get("encryptKey", ""), "verificationToken": extra.get("verificationToken", ""), - "allowFrom": extra.get("allowFrom", []), + "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), } if ctype == "dingtalk": return { "enabled": enabled, "clientId": external_app_id, "clientSecret": app_secret, - "allowFrom": extra.get("allowFrom", []), + "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), } if ctype == "telegram": return { @@ -722,7 +734,7 @@ def _channel_api_to_cfg(row: Dict[str, Any]) -> Dict[str, Any]: "token": app_secret, "proxy": extra.get("proxy", ""), "replyToMessage": bool(extra.get("replyToMessage", False)), - "allowFrom": extra.get("allowFrom", []), + "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), } if ctype == "slack": return { @@ -740,7 +752,7 @@ def _channel_api_to_cfg(row: Dict[str, Any]) -> Dict[str, Any]: "enabled": enabled, "appId": external_app_id, "secret": app_secret, - "allowFrom": extra.get("allowFrom", []), + "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), } merged = dict(extra) merged.update( diff --git a/frontend/src/App.css b/frontend/src/App.css index 9d0385e..53a8974 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -79,12 +79,43 @@ body { align-items: flex-start; } +.app-header-actions { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + +.app-header-collapsible { + transition: padding 0.2s ease; +} + +.app-header-collapsible.is-collapsed { + padding-top: 8px; + padding-bottom: 8px; + cursor: pointer; +} + +.app-header-collapsible.is-collapsed .app-header-top { + align-items: center; +} + +.app-header-toggle { + flex: 0 0 auto; +} + .app-title { display: flex; align-items: center; gap: 10px; } +.app-title-main { + display: inline-flex; + align-items: center; + gap: 8px; +} + .app-title-icon { width: 22px; height: 22px; @@ -109,6 +140,23 @@ body { font-size: 12px; } +.app-header-toggle-inline { + border: 0; + background: transparent; + color: var(--icon); + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + padding: 0; + cursor: pointer; +} + +.app-header-toggle-inline:hover { + color: var(--brand); +} + .global-switches { display: flex; gap: 8px; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 81566be..f3f556d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState, type ReactElement } from 'react'; import axios from 'axios'; -import { MoonStar, SunMedium, X } from 'lucide-react'; +import { ChevronDown, ChevronUp, MoonStar, SunMedium, X } from 'lucide-react'; import { useAppStore } from './store/appStore'; import { useBotsSync } from './hooks/useBotsSync'; import { APP_ENDPOINTS } from './config/env'; @@ -27,6 +27,8 @@ function AuthenticatedApp({ const [showCreateWizard, setShowCreateWizard] = useState(false); useBotsSync(); const t = pickLocale(locale, { 'zh-cn': appZhCn, en: appEn }); + const isSingleBotCompactView = compactMode && Boolean(String(forcedBotId || '').trim()); + const [headerCollapsed, setHeaderCollapsed] = useState(isSingleBotCompactView); useEffect(() => { const forced = String(forcedBotId || '').trim(); @@ -39,19 +41,44 @@ function AuthenticatedApp({ document.title = botName ? `${t.title} - ${botName}` : `${t.title} - ${forced}`; }, [activeBots, t.title, forcedBotId]); + useEffect(() => { + setHeaderCollapsed(isSingleBotCompactView); + }, [isSingleBotCompactView, forcedBotId]); + return (