From ec974e6694022cbadc172a96c3e19c237619974d Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Mon, 11 May 2026 18:31:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.full.example | 4 +- .env.prod.example | 4 +- README.md | 6 +- backend/providers/bot_workspace_provider.py | 32 +++++- backend/services/bot_config_service.py | 43 +++++-- backend/services/bot_management_service.py | 8 +- backend/services/bot_service.py | 60 ++++++++-- docker-compose.full.yml | 11 +- docker-compose.prod.yml | 5 +- .../DashboardChannelConfigModal.tsx | 105 +++++++++--------- .../config-managers/channelManager.ts | 75 +++---------- .../dashboard/hooks/useBotDashboardModule.ts | 1 - .../hooks/useDashboardChannelConfig.ts | 54 +-------- .../hooks/useDashboardConfigPanels.ts | 3 - .../modules/onboarding/BotWizardModule.tsx | 4 - .../components/BotWizardChannelModal.tsx | 56 ++++++---- .../modules/onboarding/hooks/useBotWizard.ts | 16 +-- scripts/deploy-full.sh | 20 ++++ scripts/deploy-prod.sh | 20 ++++ 19 files changed, 279 insertions(+), 248 deletions(-) diff --git a/.env.full.example b/.env.full.example index ee59f58..9ba1b6c 100644 --- a/.env.full.example +++ b/.env.full.example @@ -5,8 +5,8 @@ NGINX_PORT=8080 # Only workspace root still needs an absolute host path. HOST_BOTS_WORKSPACE_ROOT=/opt/dashboard-nanobot/workspace/bots -# Fixed Docker bridge subnet for the compose network. -# Change this if it conflicts with your host LAN / VPN / intranet routing. +# Shared Docker network for dashboard and bot containers. +# deploy-full.sh will reuse it when it already exists, or create it with the subnet below. DOCKER_NETWORK_NAME=dashboard-nanobot-network DOCKER_NETWORK_SUBNET=172.20.0.0/16 diff --git a/.env.prod.example b/.env.prod.example index 20438b2..acdafd8 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -5,8 +5,8 @@ NGINX_PORT=8082 # Only workspace root still needs an absolute host path. HOST_BOTS_WORKSPACE_ROOT=/dep/dashboard-nanobot/workspace/bots -# Fixed Docker bridge subnet for the compose network. -# Change this if it conflicts with your host LAN / VPN / intranet routing. +# Shared Docker network for dashboard and bot containers. +# deploy-prod.sh will reuse it when it already exists, or create it with the subnet below. DOCKER_NETWORK_NAME=dashboard-nanobot-network DOCKER_NETWORK_SUBNET=172.20.0.0/16 diff --git a/README.md b/README.md index 58b7934..766a648 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ graph TD - 复制 `.env.prod.example` 为 `.env.prod`(位于项目根目录) - `data/` 会自动映射到宿主机项目根目录下的 `./data` - `deploy-prod.sh` 现在要求使用外部 PostgreSQL,且目标库必须提前执行 `scripts/sql/create-tables.sql` 与 `scripts/sql/init-data.sql` + - `DOCKER_NETWORK_NAME` 表示 Dashboard 与 Bot 共用的 Docker network;`deploy-prod.sh` 会优先复用现有 network,不存在时按 `DOCKER_NETWORK_SUBNET` 自动创建 - 只需要配置绝对路径: - `HOST_BOTS_WORKSPACE_ROOT` - 如启用本地语音识别,请将 Whisper `.bin` 模型文件放到宿主机项目根目录的 `data/model/` @@ -122,7 +123,7 @@ graph TD - 如需基础镜像加速,覆盖 `PYTHON_BASE_IMAGE` / `NODE_BASE_IMAGE` / `NGINX_BASE_IMAGE` 2. 启动服务 - `./scripts/deploy-prod.sh` - - 或:`docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --build` + - 如需手动执行 `docker compose`,请先自行创建 `DOCKER_NETWORK_NAME` 指向的 Docker network 3. 访问 - `http://:${NGINX_PORT}`(默认 `8080`) @@ -130,6 +131,7 @@ graph TD - `backend` 不开放宿主机端口,仅在内部网络被 Nginx 访问。 - `deploy-prod.sh` 仅负责前后端容器部署,不会初始化外部数据库;外部 PostgreSQL 需要事先建表并导入初始化数据。 +- `deploy-prod.sh` 会确保 `DOCKER_NETWORK_NAME` 对应的共享 Docker network 存在,但停止服务时不会删除该 network。 - 如果启用 Redis,`REDIS_URL` 必须从 `backend` 容器内部可达;在 `docker-compose.prod.yml` 里使用 `127.0.0.1` 只会指向后端容器自己,不是宿主机。 - Redis 不可达时,通用缓存健康检查会显示 `degraded`;面板登录认证会自动回退到数据库登录态,不再因为缓存不可达直接报错。 - `UPLOAD_MAX_MB` 仅用于 Nginx 入口限制;后端业务校验值来自 `sys_setting.upload_max_mb`。 @@ -160,6 +162,7 @@ graph TD 1. 准备部署变量 - 复制 `.env.full.example` 为 `.env.full` - `data/` 会自动映射到宿主机项目根目录下的 `./data` + - `DOCKER_NETWORK_NAME` 表示 Dashboard 与 Bot 共用的 Docker network;`deploy-full.sh` 会优先复用现有 network,不存在时按 `DOCKER_NETWORK_SUBNET` 自动创建 - 必填修改: - `HOST_BOTS_WORKSPACE_ROOT` - `POSTGRES_SUPERPASSWORD` @@ -174,6 +177,7 @@ graph TD ### 初始化说明 - `scripts/deploy-full.sh` 会先启动 `postgres` / `redis`,然后自动调用 `scripts/init-full-db.sh`。 +- `scripts/deploy-full.sh` 会确保 `DOCKER_NETWORK_NAME` 对应的共享 Docker network 存在,但停止服务时不会删除该 network。 - `scripts/init-full-db.sh` 负责: - 等待 PostgreSQL 就绪 - 创建或更新业务账号 diff --git a/backend/providers/bot_workspace_provider.py b/backend/providers/bot_workspace_provider.py index e04c97d..456e6f6 100644 --- a/backend/providers/bot_workspace_provider.py +++ b/backend/providers/bot_workspace_provider.py @@ -57,6 +57,18 @@ def _normalize_extra_config(raw: Any) -> Dict[str, Any]: return dict(raw) +def _channel_delivery_flags( + extra: Dict[str, Any], + *, + default_send_progress: bool = True, + default_send_tool_hints: bool = True, +) -> Dict[str, bool]: + return { + "sendProgress": bool(extra.get("sendProgress", default_send_progress)), + "sendToolHints": bool(extra.get("sendToolHints", default_send_tool_hints)), + } + + def _write_json_atomic(path: str, payload: Dict[str, Any]) -> None: os.makedirs(os.path.dirname(path), exist_ok=True) tmp_path = f"{path}.tmp" @@ -85,8 +97,8 @@ class BotWorkspaceProvider: temperature = float(bot_data.get("temperature")) top_p = float(bot_data.get("top_p")) max_tokens = int(bot_data.get("max_tokens")) - send_progress = bool(bot_data.get("send_progress")) - send_tool_hints = bool(bot_data.get("send_tool_hints")) + send_progress = bool(bot_data.get("send_progress", True)) + send_tool_hints = bool(bot_data.get("send_tool_hints", True)) bot_root = os.path.join(self.host_data_root, bot_id) dot_nanobot_dir = os.path.join(bot_root, ".nanobot") @@ -120,13 +132,15 @@ class BotWorkspaceProvider: provider_name: provider_cfg, }, "channels": { - "sendProgress": send_progress, - "sendToolHints": send_tool_hints, + "sendProgress": True, + "sendToolHints": True, "dashboard": { "enabled": True, "host": "0.0.0.0", "port": 9000, "allowFrom": ["*"], + "sendProgress": send_progress, + "sendToolHints": send_tool_hints, }, }, } @@ -143,6 +157,7 @@ class BotWorkspaceProvider: if not channel_type or channel_type == "dashboard": continue extra = _normalize_extra_config(channel.get("extra_config")) + delivery_flags = _channel_delivery_flags(extra) enabled = bool(channel.get("is_active")) external_app_id = str(channel.get("external_app_id") or "").strip() app_secret = str(channel.get("app_secret") or "").strip() @@ -154,6 +169,7 @@ class BotWorkspaceProvider: "proxy": str(extra.get("proxy") or "").strip(), "replyToMessage": bool(extra.get("replyToMessage")), "allowFrom": _normalize_allow_from(extra.get("allowFrom")), + **delivery_flags, } continue @@ -165,6 +181,7 @@ class BotWorkspaceProvider: "encryptKey": str(extra.get("encryptKey") or "").strip(), "verificationToken": str(extra.get("verificationToken") or "").strip(), "allowFrom": _normalize_allow_from(extra.get("allowFrom")), + **delivery_flags, } continue @@ -174,6 +191,7 @@ class BotWorkspaceProvider: "clientId": external_app_id, "clientSecret": app_secret, "allowFrom": _normalize_allow_from(extra.get("allowFrom")), + **delivery_flags, } continue @@ -187,6 +205,7 @@ class BotWorkspaceProvider: "groupPolicy": str(extra.get("groupPolicy") or "mention"), "groupAllowFrom": extra.get("groupAllowFrom") if isinstance(extra.get("groupAllowFrom"), list) else [], "reactEmoji": str(extra.get("reactEmoji") or "eyes"), + **delivery_flags, } continue @@ -196,6 +215,7 @@ class BotWorkspaceProvider: "appId": external_app_id, "secret": app_secret, "allowFrom": _normalize_allow_from(extra.get("allowFrom")), + **delivery_flags, } continue @@ -205,6 +225,7 @@ class BotWorkspaceProvider: "botId": external_app_id, "secret": app_secret, "allowFrom": _normalize_allow_from(extra.get("allowFrom")), + **delivery_flags, } welcome_message = str(extra.get("welcomeMessage") or "").strip() if welcome_message: @@ -216,6 +237,7 @@ class BotWorkspaceProvider: weixin_cfg: Dict[str, Any] = { "enabled": enabled, "allowFrom": _normalize_allow_from(extra.get("allowFrom")), + **delivery_flags, } route_tag = str(extra.get("routeTag") or "").strip() if route_tag: @@ -258,6 +280,7 @@ class BotWorkspaceProvider: "maxBodyChars": max(1, int(extra.get("maxBodyChars") or 12000)), "subjectPrefix": str(extra.get("subjectPrefix") or "Re: "), "allowFrom": _normalize_allow_from(extra.get("allowFrom")), + **delivery_flags, } continue @@ -266,6 +289,7 @@ class BotWorkspaceProvider: "appId": external_app_id, "appSecret": app_secret, **extra, + **delivery_flags, } _write_json_atomic(os.path.join(dot_nanobot_dir, "config.json"), config_data) diff --git a/backend/services/bot_config_service.py b/backend/services/bot_config_service.py index e08aafe..a768892 100644 --- a/backend/services/bot_config_service.py +++ b/backend/services/bot_config_service.py @@ -18,7 +18,6 @@ from services.bot_service import ( channel_api_to_config, list_bot_channels_from_config, normalize_channel_extra, - read_global_delivery_flags, sync_bot_workspace_channels, ) from services.bot_mcp_service import ( @@ -66,6 +65,8 @@ def _read_bot_channels_cfg(bot_id: str) -> tuple[Dict[str, Any], Dict[str, Any]] if not isinstance(channels_cfg, dict): channels_cfg = {} config_data["channels"] = channels_cfg + channels_cfg["sendProgress"] = bool(channels_cfg.get("sendProgress", True)) + channels_cfg["sendToolHints"] = bool(channels_cfg.get("sendToolHints", True)) return config_data, channels_cfg @@ -311,11 +312,38 @@ def update_bot_channel_config( rows = list_bot_channels_from_config(bot) row = _find_channel_row(rows, channel_id) - if str(row.get("channel_type") or "").strip().lower() == "dashboard" or bool(row.get("locked")): - raise HTTPException(status_code=400, detail="dashboard channel is built-in and cannot be modified") update_data = payload.model_dump(exclude_unset=True) existing_type = str(row.get("channel_type") or "").strip().lower() + if existing_type == "dashboard" or bool(row.get("locked")): + if "channel_type" in update_data and str(update_data.get("channel_type") or "").strip().lower() != "dashboard": + raise HTTPException(status_code=400, detail="dashboard channel type cannot be changed") + if "is_active" in update_data and update_data["is_active"] is False: + raise HTTPException(status_code=400, detail="dashboard channel must remain enabled") + config_data, channels_cfg = _read_bot_channels_cfg(bot_id) + extra = normalize_channel_extra(update_data.get("extra_config", row.get("extra_config"))) + dashboard_cfg = channels_cfg.get("dashboard") + if not isinstance(dashboard_cfg, dict): + dashboard_cfg = {} + dashboard_cfg.update( + { + "enabled": True, + "host": str(dashboard_cfg.get("host") or "0.0.0.0"), + "port": max(1, min(int(dashboard_cfg.get("port") or 9000), 65535)), + "allowFrom": dashboard_cfg.get("allowFrom") if isinstance(dashboard_cfg.get("allowFrom"), list) else ["*"], + "sendProgress": bool(extra.get("sendProgress", dashboard_cfg.get("sendProgress", channels_cfg.get("sendProgress", True)))), + "sendToolHints": bool(extra.get("sendToolHints", dashboard_cfg.get("sendToolHints", channels_cfg.get("sendToolHints", True)))), + } + ) + channels_cfg["dashboard"] = dashboard_cfg + row["is_active"] = True + row["extra_config"] = { + "sendProgress": bool(dashboard_cfg.get("sendProgress")), + "sendToolHints": bool(dashboard_cfg.get("sendToolHints")), + } + _write_bot_config_state(session, bot_id=bot_id, config_data=config_data, sync_workspace=True) + return row + new_type = existing_type if "channel_type" in update_data and update_data["channel_type"] is not None: new_type = str(update_data["channel_type"]).strip().lower() @@ -344,15 +372,6 @@ def update_bot_channel_config( row["locked"] = new_type == "dashboard" config_data, channels_cfg = _read_bot_channels_cfg(bot_id) - current_send_progress, current_send_tool_hints = read_global_delivery_flags(channels_cfg) - if new_type == "dashboard": - extra = normalize_channel_extra(row.get("extra_config")) - channels_cfg["sendProgress"] = bool(extra.get("sendProgress", current_send_progress)) - channels_cfg["sendToolHints"] = bool(extra.get("sendToolHints", current_send_tool_hints)) - else: - channels_cfg["sendProgress"] = current_send_progress - channels_cfg["sendToolHints"] = current_send_tool_hints - channels_cfg.pop("dashboard", None) if existing_type != "dashboard" and existing_type in channels_cfg and existing_type != new_type: channels_cfg.pop(existing_type, None) if new_type != "dashboard": diff --git a/backend/services/bot_management_service.py b/backend/services/bot_management_service.py index 4e8859a..d41005e 100644 --- a/backend/services/bot_management_service.py +++ b/backend/services/bot_management_service.py @@ -135,8 +135,8 @@ def create_bot_record(session: Session, *, payload: BotCreateRequest) -> Dict[st normalized_bot_id, channels_override=normalize_initial_bot_channels(normalized_bot_id, payload.channels), global_delivery_override={ - "sendProgress": bool(payload.send_progress) if payload.send_progress is not None else False, - "sendToolHints": bool(payload.send_tool_hints) if payload.send_tool_hints is not None else False, + "sendProgress": bool(payload.send_progress) if payload.send_progress is not None else True, + "sendToolHints": bool(payload.send_tool_hints) if payload.send_tool_hints is not None else True, }, runtime_overrides={ "llm_provider": llm_provider, @@ -154,8 +154,8 @@ def create_bot_record(session: Session, *, payload: BotCreateRequest) -> Dict[st "user_md": payload.user_md, "tools_md": payload.tools_md, "identity_md": payload.identity_md, - "send_progress": bool(payload.send_progress) if payload.send_progress is not None else False, - "send_tool_hints": bool(payload.send_tool_hints) if payload.send_tool_hints is not None else False, + "send_progress": bool(payload.send_progress) if payload.send_progress is not None else True, + "send_tool_hints": bool(payload.send_tool_hints) if payload.send_tool_hints is not None else True, }, ) record_activity_event( diff --git a/backend/services/bot_service.py b/backend/services/bot_service.py index 27e3357..c5c6dbb 100644 --- a/backend/services/bot_service.py +++ b/backend/services/bot_service.py @@ -62,8 +62,16 @@ def _normalize_allow_from(raw: Any) -> List[str]: def read_global_delivery_flags(channels_cfg: Any) -> tuple[bool, bool]: if not isinstance(channels_cfg, dict): - return False, False - return bool(channels_cfg.get("sendProgress")), bool(channels_cfg.get("sendToolHints")) + return True, True + return bool(channels_cfg.get("sendProgress", True)), bool(channels_cfg.get("sendToolHints", True)) + + +def _delivery_extra(channels_cfg: Any, cfg: Dict[str, Any]) -> Dict[str, bool]: + default_send_progress, default_send_tool_hints = read_global_delivery_flags(channels_cfg) + return { + "sendProgress": bool(cfg.get("sendProgress", default_send_progress)), + "sendToolHints": bool(cfg.get("sendToolHints", default_send_tool_hints)), + } def channel_config_to_api(bot_id: str, channel_type: str, cfg: Dict[str, Any]) -> Dict[str, Any]: @@ -81,17 +89,19 @@ def channel_config_to_api(bot_id: str, channel_type: str, cfg: Dict[str, Any]) - "encryptKey": cfg.get("encryptKey", ""), "verificationToken": cfg.get("verificationToken", ""), "allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])), + **_delivery_extra({"sendProgress": True, "sendToolHints": True}, cfg), } elif ctype == "dingtalk": external_app_id = str(cfg.get("clientId") or "") app_secret = str(cfg.get("clientSecret") or "") - extra = {"allowFrom": _normalize_allow_from(cfg.get("allowFrom", []))} + extra = {"allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])), **_delivery_extra({"sendProgress": True, "sendToolHints": True}, cfg)} elif ctype == "telegram": app_secret = str(cfg.get("token") or "") extra = { "proxy": cfg.get("proxy", ""), "replyToMessage": bool(cfg.get("replyToMessage", False)), "allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])), + **_delivery_extra({"sendProgress": True, "sendToolHints": True}, cfg), } elif ctype == "slack": external_app_id = str(cfg.get("botToken") or "") @@ -102,22 +112,25 @@ def channel_config_to_api(bot_id: str, channel_type: str, cfg: Dict[str, Any]) - "groupPolicy": cfg.get("groupPolicy", "mention"), "groupAllowFrom": cfg.get("groupAllowFrom", []), "reactEmoji": cfg.get("reactEmoji", "eyes"), + **_delivery_extra({"sendProgress": True, "sendToolHints": True}, cfg), } elif ctype == "qq": external_app_id = str(cfg.get("appId") or "") app_secret = str(cfg.get("secret") or "") - extra = {"allowFrom": _normalize_allow_from(cfg.get("allowFrom", []))} + extra = {"allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])), **_delivery_extra({"sendProgress": True, "sendToolHints": True}, cfg)} elif ctype == "wecom": external_app_id = str(cfg.get("botId") or "") app_secret = str(cfg.get("secret") or "") extra = { "allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])), "welcomeMessage": str(cfg.get("welcomeMessage") or ""), + **_delivery_extra({"sendProgress": True, "sendToolHints": True}, cfg), } elif ctype == "weixin": app_secret = "" extra = { "hasSavedState": (Path(BOTS_WORKSPACE_ROOT) / bot_id / ".nanobot" / "weixin" / "account.json").is_file(), + **_delivery_extra({"sendProgress": True, "sendToolHints": True}, cfg), } elif ctype == "email": extra = { @@ -141,6 +154,7 @@ def channel_config_to_api(bot_id: str, channel_type: str, cfg: Dict[str, Any]) - "maxBodyChars": int(cfg.get("maxBodyChars") or 12000), "subjectPrefix": str(cfg.get("subjectPrefix") or "Re: "), "allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])), + **_delivery_extra({"sendProgress": True, "sendToolHints": True}, cfg), } else: external_app_id = str( @@ -170,8 +184,11 @@ def channel_config_to_api(bot_id: str, channel_type: str, cfg: Dict[str, Any]) - "secret", "token", "appToken", + "sendProgress", + "sendToolHints", } } + extra.update(_delivery_extra({"sendProgress": True, "sendToolHints": True}, cfg)) return { "id": ctype, @@ -202,6 +219,8 @@ def channel_api_to_config(row: Dict[str, Any]) -> Dict[str, Any]: "encryptKey": extra.get("encryptKey", ""), "verificationToken": extra.get("verificationToken", ""), "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), + "sendProgress": bool(extra.get("sendProgress", True)), + "sendToolHints": bool(extra.get("sendToolHints", True)), } if ctype == "dingtalk": return { @@ -209,6 +228,8 @@ def channel_api_to_config(row: Dict[str, Any]) -> Dict[str, Any]: "clientId": external_app_id, "clientSecret": app_secret, "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), + "sendProgress": bool(extra.get("sendProgress", True)), + "sendToolHints": bool(extra.get("sendToolHints", True)), } if ctype == "telegram": return { @@ -217,6 +238,8 @@ def channel_api_to_config(row: Dict[str, Any]) -> Dict[str, Any]: "proxy": extra.get("proxy", ""), "replyToMessage": bool(extra.get("replyToMessage", False)), "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), + "sendProgress": bool(extra.get("sendProgress", True)), + "sendToolHints": bool(extra.get("sendToolHints", True)), } if ctype == "slack": return { @@ -228,6 +251,8 @@ def channel_api_to_config(row: Dict[str, Any]) -> Dict[str, Any]: "groupPolicy": extra.get("groupPolicy", "mention"), "groupAllowFrom": extra.get("groupAllowFrom", []), "reactEmoji": extra.get("reactEmoji", "eyes"), + "sendProgress": bool(extra.get("sendProgress", True)), + "sendToolHints": bool(extra.get("sendToolHints", True)), } if ctype == "qq": return { @@ -235,6 +260,8 @@ def channel_api_to_config(row: Dict[str, Any]) -> Dict[str, Any]: "appId": external_app_id, "secret": app_secret, "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), + "sendProgress": bool(extra.get("sendProgress", True)), + "sendToolHints": bool(extra.get("sendToolHints", True)), } if ctype == "wecom": return { @@ -243,11 +270,15 @@ def channel_api_to_config(row: Dict[str, Any]) -> Dict[str, Any]: "secret": app_secret, "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), "welcomeMessage": str(extra.get("welcomeMessage") or ""), + "sendProgress": bool(extra.get("sendProgress", True)), + "sendToolHints": bool(extra.get("sendToolHints", True)), } if ctype == "weixin": return { "enabled": enabled, "token": app_secret, + "sendProgress": bool(extra.get("sendProgress", True)), + "sendToolHints": bool(extra.get("sendToolHints", True)), } if ctype == "email": return { @@ -272,6 +303,8 @@ def channel_api_to_config(row: Dict[str, Any]) -> Dict[str, Any]: "maxBodyChars": max(1, int(extra.get("maxBodyChars") or 12000)), "subjectPrefix": str(extra.get("subjectPrefix") or "Re: "), "allowFrom": _normalize_allow_from(extra.get("allowFrom", [])), + "sendProgress": bool(extra.get("sendProgress", True)), + "sendToolHints": bool(extra.get("sendToolHints", True)), } merged = dict(extra) merged.update( @@ -290,7 +323,8 @@ def list_bot_channels_from_config(bot: BotInstance) -> List[Dict[str, Any]]: channels_cfg = config_data.get("channels") if not isinstance(channels_cfg, dict): channels_cfg = {} - send_progress, send_tool_hints = read_global_delivery_flags(channels_cfg) + dashboard_cfg = channels_cfg.get("dashboard") + dashboard_extra = _delivery_extra(channels_cfg, dashboard_cfg if isinstance(dashboard_cfg, dict) else {}) rows: List[Dict[str, Any]] = [ { "id": "dashboard", @@ -300,17 +334,19 @@ def list_bot_channels_from_config(bot: BotInstance) -> List[Dict[str, Any]]: "app_secret": "", "internal_port": 9000, "is_active": True, - "extra_config": { - "sendProgress": send_progress, - "sendToolHints": send_tool_hints, - }, + "extra_config": dashboard_extra, "locked": True, } ] for ctype, cfg in channels_cfg.items(): if ctype in {"sendProgress", "sendToolHints", "dashboard"} or not isinstance(cfg, dict): continue - rows.append(channel_config_to_api(bot.id, ctype, cfg)) + row = channel_config_to_api(bot.id, ctype, cfg) + row["extra_config"] = { + **row.get("extra_config", {}), + **_delivery_extra(channels_cfg, cfg), + } + rows.append(row) return rows @@ -513,8 +549,8 @@ def sync_bot_workspace_channels( bot_data.get("storage_gb"), ) bot_data.update(resources) - send_progress = bool(bot_data.get("send_progress", False)) - send_tool_hints = bool(bot_data.get("send_tool_hints", False)) + send_progress = bool(bot_data.get("send_progress", True)) + send_tool_hints = bool(bot_data.get("send_tool_hints", True)) if isinstance(global_delivery_override, dict): if "sendProgress" in global_delivery_override: send_progress = bool(global_delivery_override.get("sendProgress")) diff --git a/docker-compose.full.yml b/docker-compose.full.yml index 641d118..780eeb0 100644 --- a/docker-compose.full.yml +++ b/docker-compose.full.yml @@ -66,7 +66,7 @@ services: environment: TZ: ${TZ:-Asia/Shanghai} APP_HOST: 0.0.0.0 - APP_PORT: 8000 + APP_PORT: 8002 APP_RELOAD: "false" DATABASE_ECHO: "false" DATABASE_POOL_SIZE: ${DATABASE_POOL_SIZE:-20} @@ -101,9 +101,9 @@ services: - ./data:/app/data - ${HOST_BOTS_WORKSPACE_ROOT}:${HOST_BOTS_WORKSPACE_ROOT} expose: - - "8000" + - "8002" healthcheck: - test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=3).read()"] + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8002/api/health', timeout=3).read()"] interval: 15s timeout: 5s retries: 5 @@ -150,7 +150,4 @@ services: networks: default: name: ${DOCKER_NETWORK_NAME:-dashboard-nanobot-network} - driver: bridge - ipam: - config: - - subnet: ${DOCKER_NETWORK_SUBNET:-172.20.0.0/16} + external: true diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 27273f5..a35f3fa 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -95,7 +95,4 @@ services: networks: default: name: ${DOCKER_NETWORK_NAME:-dashboard-nanobot-network} - driver: bridge - ipam: - config: - - subnet: ${DOCKER_NETWORK_SUBNET:-172.20.0.0/16} + external: true diff --git a/frontend/src/modules/dashboard/components/DashboardChannelConfigModal.tsx b/frontend/src/modules/dashboard/components/DashboardChannelConfigModal.tsx index 6cf5bb1..8cf4664 100644 --- a/frontend/src/modules/dashboard/components/DashboardChannelConfigModal.tsx +++ b/frontend/src/modules/dashboard/components/DashboardChannelConfigModal.tsx @@ -53,6 +53,40 @@ function isChannelConfigured(channel: BotChannel): boolean { return Boolean(String(channel.external_app_id || '').trim() || String(channel.app_secret || '').trim()); } +function ChannelDeliveryFields({ + channel, + labels, + onPatch, +}: { + channel: BotChannel; + labels: Pick; + onPatch: (patch: Partial) => void; +}) { + const extra = channel.extra_config || {}; + return ( + <> + + + + ); +} + function ChannelFieldsEditor({ channel, labels, @@ -302,23 +336,18 @@ function ChannelFieldsEditor({ interface ChannelConfigModalProps { open: boolean; channels: BotChannel[]; - globalDelivery: { sendProgress: boolean; sendToolHints: boolean }; expandedChannelByKey: Record; newChannelDraft: BotChannel; addableChannelTypes: ChannelType[]; newChannelPanelOpen: boolean; channelCreateMenuOpen: boolean; channelCreateMenuRef: RefObject; - isSavingGlobalDelivery: boolean; isSavingChannel: boolean; weixinLoginStatus: WeixinLoginStatus | null; - hasSelectedBot: boolean; isZh: boolean; labels: ChannelConfigLabels; passwordToggleLabels: PasswordToggleLabels; onClose: () => void; - onUpdateGlobalDeliveryFlag: (key: 'sendProgress' | 'sendToolHints', value: boolean) => void; - onSaveGlobalDelivery: () => Promise | void; getChannelUiKey: (channel: Pick, fallbackIndex: number) => string; isDashboardChannel: (channel: BotChannel) => boolean; onUpdateChannelLocal: (index: number, patch: Partial) => void; @@ -337,23 +366,18 @@ interface ChannelConfigModalProps { export function ChannelConfigModal({ open, channels, - globalDelivery, expandedChannelByKey, newChannelDraft, addableChannelTypes, newChannelPanelOpen, channelCreateMenuOpen, channelCreateMenuRef, - isSavingGlobalDelivery, isSavingChannel, weixinLoginStatus, - hasSelectedBot, isZh, labels, passwordToggleLabels, onClose, - onUpdateGlobalDeliveryFlag, - onSaveGlobalDelivery, getChannelUiKey, isDashboardChannel, onUpdateChannelLocal, @@ -410,7 +434,7 @@ export function ChannelConfigModal({
-
-
{labels.globalDeliveryTitle}
-
- - - void onSaveGlobalDelivery()} - tooltip={labels.saveChannel} - aria-label={labels.saveChannel} - > - - -
-
- {channels.filter((channel) => !isDashboardChannel(channel)).length === 0 ? ( + {channels.length === 0 ? (
{labels.channelEmpty}
) : ( channels.map((channel, idx) => { - if (isDashboardChannel(channel)) return null; + const dashboardChannel = isDashboardChannel(channel); const uiKey = getChannelUiKey(channel, idx); const expanded = expandedChannelByKey[uiKey] ?? idx === 0; const summary = [ String(channel.channel_type || '').toUpperCase(), channel.is_active ? labels.enabled : labels.disabled, - isChannelConfigured(channel) ? labels.channelConfigured : labels.channelPending, + dashboardChannel ? labels.dashboardLocked : (isChannelConfigured(channel) ? labels.channelConfigured : labels.channelPending), ].join(' · '); return (
@@ -499,6 +491,7 @@ export function ChannelConfigModal({ onUpdateChannelLocal(idx, { is_active: e.target.checked })} style={{ marginRight: 6 }} /> @@ -506,7 +499,7 @@ export function ChannelConfigModal({ void onRemoveChannel(channel)} tooltip={labels.remove} aria-label={labels.remove} @@ -526,16 +519,23 @@ export function ChannelConfigModal({ {expanded ? ( <>
- onUpdateChannelLocal(idx, patch)} + /> + )} + onUpdateChannelLocal(idx, patch)} />
{renderWeixinLoginBlock(channel)}
- {labels.customChannel} + {dashboardChannel ? labels.defaultChannel : labels.customChannel}
{labels.channelAddHint} diff --git a/frontend/src/modules/dashboard/config-managers/channelManager.ts b/frontend/src/modules/dashboard/config-managers/channelManager.ts index 80a84aa..cf3a189 100644 --- a/frontend/src/modules/dashboard/config-managers/channelManager.ts +++ b/frontend/src/modules/dashboard/config-managers/channelManager.ts @@ -34,11 +34,6 @@ export interface ChannelManagerLabels { channels: string; } -export interface GlobalDeliveryState { - sendProgress: boolean; - sendToolHints: boolean; -} - interface ApiErrorDetail { detail?: string; } @@ -58,12 +53,9 @@ function resolveApiErrorMessage(error: unknown, fallback: string): string { interface ChannelManagerDeps extends PromptApi { selectedBotId: string; - selectedBotDockerStatus: string; t: ChannelManagerLabels; - currentGlobalDelivery: GlobalDeliveryState; addableChannelTypes: ChannelType[]; currentNewChannelDraft: BotChannel; - refresh: () => Promise; setShowChannelModal: (value: boolean) => void; setChannels: (value: BotChannel[] | ((prev: BotChannel[]) => BotChannel[])) => void; setExpandedChannelByKey: ( @@ -73,20 +65,13 @@ interface ChannelManagerDeps extends PromptApi { setNewChannelPanelOpen: (value: boolean) => void; setNewChannelDraft: (value: BotChannel | ((prev: BotChannel) => BotChannel)) => void; setIsSavingChannel: (value: boolean) => void; - setGlobalDelivery: ( - value: GlobalDeliveryState | ((prev: GlobalDeliveryState) => GlobalDeliveryState) - ) => void; - setIsSavingGlobalDelivery: (value: boolean) => void; } export function createChannelManager({ selectedBotId, - selectedBotDockerStatus, t, - currentGlobalDelivery, addableChannelTypes, currentNewChannelDraft, - refresh, notify, confirm, setShowChannelModal, @@ -96,11 +81,11 @@ export function createChannelManager({ setNewChannelPanelOpen, setNewChannelDraft, setIsSavingChannel, - setGlobalDelivery, - setIsSavingGlobalDelivery, }: ChannelManagerDeps) { - const createEmptyChannelExtra = (channelType: ChannelType): Record => - channelType === 'weixin' ? {} : {}; + const createEmptyChannelExtra = (): Record => ({ + sendProgress: true, + sendToolHints: true, + }); const createEmptyChannelDraft = (channelType: ChannelType = 'feishu'): BotChannel => ({ id: 'draft-channel', @@ -110,7 +95,7 @@ export function createChannelManager({ app_secret: '', internal_port: 8080, is_active: true, - extra_config: createEmptyChannelExtra(channelType), + extra_config: createEmptyChannelExtra(), }); const channelDraftUiKey = (channel: Pick, fallbackIndex: number) => { @@ -129,10 +114,9 @@ export function createChannelManager({ const sanitizeChannelExtra = (channelType: string, extra: Record) => { const type = String(channelType || '').toLowerCase(); if (type === 'dashboard') return extra || {}; - if (type === 'weixin') return {}; const next = { ...(extra || {}) }; - delete next.sendProgress; - delete next.sendToolHints; + next.sendProgress = Boolean(next.sendProgress); + next.sendToolHints = Boolean(next.sendToolHints); return next; }; @@ -143,12 +127,10 @@ export function createChannelManager({ setChannels(rows); setExpandedChannelByKey((prev) => { const next: Record = {}; - rows - .filter((channel) => !isDashboardChannel(channel)) - .forEach((channel, index) => { - const key = channelDraftUiKey(channel, index); - next[key] = typeof prev[key] === 'boolean' ? prev[key] : index === 0; - }); + rows.forEach((channel, index) => { + const key = channelDraftUiKey(channel, index); + next[key] = typeof prev[key] === 'boolean' ? prev[key] : index === 0; + }); return next; }); }; @@ -173,14 +155,15 @@ export function createChannelManager({ const updateChannelLocal = (index: number, patch: Partial) => { setChannels((prev) => prev.map((channel, channelIndex) => { - if (channelIndex !== index || channel.locked) return channel; + if (channelIndex !== index) return channel; return { ...channel, ...patch }; }), ); }; const saveChannel = async (channel: BotChannel) => { - if (!selectedBotId || channel.locked || isDashboardChannel(channel)) return; + if (!selectedBotId) return; + const dashboardChannel = isDashboardChannel(channel); setIsSavingChannel(true); try { await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/channels/${channel.id}`, { @@ -188,7 +171,7 @@ export function createChannelManager({ external_app_id: channel.external_app_id, app_secret: channel.app_secret, internal_port: Number(channel.internal_port), - is_active: channel.is_active, + is_active: dashboardChannel ? true : channel.is_active, extra_config: sanitizeChannelExtra(String(channel.channel_type), channel.extra_config || {}), }); await loadChannels(selectedBotId); @@ -247,32 +230,6 @@ export function createChannelManager({ } }; - const updateGlobalDeliveryFlag = (key: 'sendProgress' | 'sendToolHints', value: boolean) => { - setGlobalDelivery((prev) => ({ ...prev, [key]: value })); - }; - - const saveGlobalDelivery = async () => { - if (!selectedBotId) return; - setIsSavingGlobalDelivery(true); - try { - await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}`, { - send_progress: Boolean(currentGlobalDelivery.sendProgress), - send_tool_hints: Boolean(currentGlobalDelivery.sendToolHints), - }); - if (selectedBotDockerStatus === 'RUNNING') { - await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/stop`); - await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/start`); - } - await refresh(); - notify(t.channelSaved, { tone: 'success' }); - } catch (error: unknown) { - const message = resolveApiErrorMessage(error, t.channelSaveFail); - notify(message, { tone: 'error' }); - } finally { - setIsSavingGlobalDelivery(false); - } - }; - return { createEmptyChannelDraft, channelDraftUiKey, @@ -285,7 +242,5 @@ export function createChannelManager({ saveChannel, addChannel, removeChannel, - updateGlobalDeliveryFlag, - saveGlobalDelivery, }; } diff --git a/frontend/src/modules/dashboard/hooks/useBotDashboardModule.ts b/frontend/src/modules/dashboard/hooks/useBotDashboardModule.ts index 481e82c..3df236e 100644 --- a/frontend/src/modules/dashboard/hooks/useBotDashboardModule.ts +++ b/frontend/src/modules/dashboard/hooks/useBotDashboardModule.ts @@ -283,7 +283,6 @@ export function useBotDashboardModule({ notify, onPickSkillZip, passwordToggleLabels, - refresh, reloginWeixin, removeBotSkill, resetSupportState, diff --git a/frontend/src/modules/dashboard/hooks/useDashboardChannelConfig.ts b/frontend/src/modules/dashboard/hooks/useDashboardChannelConfig.ts index 96deb56..71887d9 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardChannelConfig.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardChannelConfig.ts @@ -1,9 +1,9 @@ -import { useCallback, useEffect, useMemo, useRef, useState, type SetStateAction } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { BotState } from '../../../types/bot'; import type { ChannelLabels } from '../localeTypes'; import { optionalChannelTypes } from '../constants'; -import { createChannelManager, type ChannelManagerLabels, type GlobalDeliveryState } from '../config-managers/channelManager'; +import { createChannelManager, type ChannelManagerLabels } from '../config-managers/channelManager'; import type { BotChannel, WeixinLoginStatus } from '../types'; type PromptTone = 'info' | 'success' | 'warning' | 'error'; @@ -29,30 +29,14 @@ interface UseDashboardChannelConfigOptions { loadWeixinLoginStatus: (botId: string, silent?: boolean) => Promise; notify: (message: string, options?: NotifyOptions) => void; passwordToggleLabels: { show: string; hide: string }; - refresh: () => Promise; reloginWeixin: () => Promise; - selectedBot?: Pick | null; + selectedBot?: Pick | null; selectedBotId: string; t: ChannelManagerLabels & { cancel: string; close: string }; lc: ChannelLabels; weixinLoginStatus: WeixinLoginStatus | null; } -const EMPTY_GLOBAL_DELIVERY: GlobalDeliveryState = { - sendProgress: false, - sendToolHints: false, -}; - -function readBotGlobalDelivery( - bot?: Pick | null, -): GlobalDeliveryState { - if (!bot) return EMPTY_GLOBAL_DELIVERY; - return { - sendProgress: Boolean(bot.send_progress), - sendToolHints: Boolean(bot.send_tool_hints), - }; -} - export function useDashboardChannelConfig({ closeRuntimeMenu, confirm, @@ -60,7 +44,6 @@ export function useDashboardChannelConfig({ loadWeixinLoginStatus, notify, passwordToggleLabels, - refresh, reloginWeixin, selectedBot, selectedBotId, @@ -82,31 +65,15 @@ export function useDashboardChannelConfig({ app_secret: '', internal_port: 8080, is_active: true, - extra_config: {}, + extra_config: { sendProgress: true, sendToolHints: true }, }); const [isSavingChannel, setIsSavingChannel] = useState(false); - const [isSavingGlobalDelivery, setIsSavingGlobalDelivery] = useState(false); - const [globalDeliveryDraftByBot, setGlobalDeliveryDraftByBot] = useState>({}); const addableChannelTypes = useMemo(() => { const exists = new Set(channels.map((channel) => String(channel.channel_type).toLowerCase())); return optionalChannelTypes.filter((type) => !exists.has(type)); }, [channels]); - const globalDelivery = useMemo(() => { - if (!selectedBotId || !selectedBot) return EMPTY_GLOBAL_DELIVERY; - return globalDeliveryDraftByBot[selectedBotId] ?? readBotGlobalDelivery(selectedBot); - }, [globalDeliveryDraftByBot, selectedBot, selectedBotId]); - - const setGlobalDelivery = useCallback((value: SetStateAction) => { - if (!selectedBotId) return; - setGlobalDeliveryDraftByBot((prev) => { - const currentValue = prev[selectedBotId] ?? readBotGlobalDelivery(selectedBot); - const nextValue = typeof value === 'function' ? value(currentValue) : value; - return { ...prev, [selectedBotId]: nextValue }; - }); - }, [selectedBot, selectedBotId]); - const { resetNewChannelDraft, channelDraftUiKey, @@ -117,16 +84,11 @@ export function useDashboardChannelConfig({ saveChannel, addChannel, removeChannel, - updateGlobalDeliveryFlag, - saveGlobalDelivery, } = createChannelManager({ selectedBotId, - selectedBotDockerStatus: selectedBot?.docker_status || '', t, - currentGlobalDelivery: globalDelivery, addableChannelTypes, currentNewChannelDraft: newChannelDraft, - refresh, notify, confirm, setShowChannelModal, @@ -136,8 +98,6 @@ export function useDashboardChannelConfig({ setNewChannelPanelOpen, setNewChannelDraft, setIsSavingChannel, - setGlobalDelivery, - setIsSavingGlobalDelivery, }); useEffect(() => { @@ -172,23 +132,19 @@ export function useDashboardChannelConfig({ setNewChannelPanelOpen(false); setChannelCreateMenuOpen(false); resetNewChannelDraft(); - setGlobalDeliveryDraftByBot({}); }, [resetNewChannelDraft]); const channelConfigModalProps = { open: showChannelModal, channels, - globalDelivery, expandedChannelByKey, newChannelDraft, addableChannelTypes, newChannelPanelOpen, channelCreateMenuOpen, channelCreateMenuRef, - isSavingGlobalDelivery, isSavingChannel, weixinLoginStatus, - hasSelectedBot: Boolean(selectedBot), isZh, labels: { ...lc, cancel: t.cancel, close: t.close }, passwordToggleLabels, @@ -198,8 +154,6 @@ export function useDashboardChannelConfig({ setNewChannelPanelOpen(false); resetNewChannelDraft(); }, - onUpdateGlobalDeliveryFlag: updateGlobalDeliveryFlag, - onSaveGlobalDelivery: saveGlobalDelivery, getChannelUiKey: channelDraftUiKey, isDashboardChannel, onUpdateChannelLocal: updateChannelLocal, diff --git a/frontend/src/modules/dashboard/hooks/useDashboardConfigPanels.ts b/frontend/src/modules/dashboard/hooks/useDashboardConfigPanels.ts index 351e964..25d1bae 100644 --- a/frontend/src/modules/dashboard/hooks/useDashboardConfigPanels.ts +++ b/frontend/src/modules/dashboard/hooks/useDashboardConfigPanels.ts @@ -50,7 +50,6 @@ interface UseDashboardConfigPanelsOptions { notify: (message: string, options?: NotifyOptions) => void; onPickSkillZip: (event: ChangeEvent) => Promise; passwordToggleLabels: { show: string; hide: string }; - refresh: () => Promise; reloginWeixin: () => Promise; removeBotSkill: (skill: WorkspaceSkillOption) => Promise; resetSupportState: () => void; @@ -92,7 +91,6 @@ export function useDashboardConfigPanels({ notify, onPickSkillZip, passwordToggleLabels, - refresh, reloginWeixin, removeBotSkill, resetSupportState, @@ -159,7 +157,6 @@ export function useDashboardConfigPanels({ loadWeixinLoginStatus, notify, passwordToggleLabels, - refresh, reloginWeixin, selectedBot, selectedBotId, diff --git a/frontend/src/modules/onboarding/BotWizardModule.tsx b/frontend/src/modules/onboarding/BotWizardModule.tsx index cdfef01..2e93176 100644 --- a/frontend/src/modules/onboarding/BotWizardModule.tsx +++ b/frontend/src/modules/onboarding/BotWizardModule.tsx @@ -83,7 +83,6 @@ export function BotWizardModule({ onCreated, onGoDashboard, onClose, showHeader testProvider, testResult, updateChannel, - updateGlobalDeliveryFlag, upsertEnvParam, } = useBotWizard({ ui, @@ -207,12 +206,9 @@ export function BotWizardModule({ onCreated, onGoDashboard, onClose, showHeader lc={lc} passwordToggleLabels={passwordToggleLabels} channels={form.channels} - sendProgress={Boolean(form.send_progress)} - sendToolHints={Boolean(form.send_tool_hints)} addableChannelTypes={addableChannelTypes} newChannelType={newChannelType} onClose={() => setShowChannelModal(false)} - onUpdateGlobalDeliveryFlag={updateGlobalDeliveryFlag} onUpdateChannel={updateChannel} onRemoveChannel={removeChannel} onSetNewChannelType={setNewChannelType} diff --git a/frontend/src/modules/onboarding/components/BotWizardChannelModal.tsx b/frontend/src/modules/onboarding/components/BotWizardChannelModal.tsx index aa19afb..08074a7 100644 --- a/frontend/src/modules/onboarding/components/BotWizardChannelModal.tsx +++ b/frontend/src/modules/onboarding/components/BotWizardChannelModal.tsx @@ -12,12 +12,9 @@ interface BotWizardChannelModalProps { lc: OnboardingChannelLabels; passwordToggleLabels: { show: string; hide: string }; channels: WizardChannelConfig[]; - sendProgress: boolean; - sendToolHints: boolean; addableChannelTypes: ChannelType[]; newChannelType: ChannelType | ''; onClose: () => void; - onUpdateGlobalDeliveryFlag: (key: 'sendProgress' | 'sendToolHints', value: boolean) => void; onUpdateChannel: (index: number, patch: Partial) => void; onRemoveChannel: (index: number) => void; onSetNewChannelType: (value: ChannelType | '') => void; @@ -135,17 +132,50 @@ function renderChannelFields({ return null; } +function renderChannelDeliveryFields({ + channel, + idx, + lc, + onUpdateChannel, +}: { + channel: WizardChannelConfig; + idx: number; + lc: Pick; + onUpdateChannel: (index: number, patch: Partial) => void; +}) { + const extra = channel.extra_config || {}; + return ( +
+ + +
+ ); +} + export function BotWizardChannelModal({ open, lc, passwordToggleLabels, channels, - sendProgress, - sendToolHints, addableChannelTypes, newChannelType, onClose, - onUpdateGlobalDeliveryFlag, onUpdateChannel, onRemoveChannel, onSetNewChannelType, @@ -157,19 +187,6 @@ export function BotWizardChannelModal({
e.stopPropagation()}>

{lc.wizardSectionTitle}

-
-
{lc.globalDeliveryTitle}
-
- - -
-
{channels.map((channel, idx) => (
@@ -187,6 +204,7 @@ export function BotWizardChannelModal({
{renderChannelFields({ channel, idx, lc, passwordToggleLabels, onUpdateChannel })} + {renderChannelDeliveryFields({ channel, idx, lc, onUpdateChannel })}
))}
diff --git a/frontend/src/modules/onboarding/hooks/useBotWizard.ts b/frontend/src/modules/onboarding/hooks/useBotWizard.ts index 8d616cf..c3a65a8 100644 --- a/frontend/src/modules/onboarding/hooks/useBotWizard.ts +++ b/frontend/src/modules/onboarding/hooks/useBotWizard.ts @@ -64,8 +64,8 @@ function clampStorageGb(value: number) { function sanitizeChannelExtra(extra: Record) { const next = { ...(extra || {}) }; - delete next.sendProgress; - delete next.sendToolHints; + next.sendProgress = Boolean(next.sendProgress); + next.sendToolHints = Boolean(next.sendToolHints); return next; } @@ -349,8 +349,6 @@ export function useBotWizard({ user_md: form.user_md, tools_md: form.tools_md, identity_md: form.identity_md, - send_progress: Boolean(form.send_progress), - send_tool_hints: Boolean(form.send_tool_hints), channels: form.channels.map((channel) => ({ channel_type: channel.channel_type, is_active: channel.is_active, @@ -426,7 +424,7 @@ export function useBotWizard({ const addChannel = () => { if (!newChannelType || !addableChannelTypes.includes(newChannelType)) return; - const initialExtraConfig = {}; + const initialExtraConfig = { sendProgress: true, sendToolHints: true }; setForm((prev) => ({ ...prev, channels: [ @@ -458,13 +456,6 @@ export function useBotWizard({ })); }; - const updateGlobalDeliveryFlag = (key: 'sendProgress' | 'sendToolHints', value: boolean) => { - setForm((prev) => { - if (key === 'sendProgress') return { ...prev, send_progress: value }; - return { ...prev, send_tool_hints: value }; - }); - }; - const upsertEnvParam = (key: string, value: string) => { const normalized = String(key || '').trim().toUpperCase(); if (!normalized) return; @@ -547,7 +538,6 @@ export function useBotWizard({ testProvider, testResult, updateChannel, - updateGlobalDeliveryFlag, upsertEnvParam, }; } diff --git a/scripts/deploy-full.sh b/scripts/deploy-full.sh index 70ce774..9cfdb9f 100755 --- a/scripts/deploy-full.sh +++ b/scripts/deploy-full.sh @@ -81,6 +81,23 @@ load_env_var() { printf -v "$name" '%s' "$value" } +ensure_docker_network() { + local network_name="$1" + local network_subnet="${2:-}" + + if docker network inspect "$network_name" >/dev/null 2>&1; then + echo "[deploy-full] reusing docker network: $network_name" + return 0 + fi + + echo "[deploy-full] creating docker network: $network_name" + if [[ -n "$network_subnet" ]]; then + docker network create --driver bridge --subnet "$network_subnet" "$network_name" >/dev/null + else + docker network create --driver bridge "$network_name" >/dev/null + fi +} + wait_for_health() { local container_name="$1" local timeout_seconds="$2" @@ -119,6 +136,8 @@ load_env_var POSTGRES_APP_DB load_env_var POSTGRES_APP_USER load_env_var POSTGRES_APP_PASSWORD load_env_var NGINX_PORT 8080 +load_env_var DOCKER_NETWORK_NAME dashboard-nanobot-network +load_env_var DOCKER_NETWORK_SUBNET 172.20.0.0/16 require_env HOST_BOTS_WORKSPACE_ROOT require_env POSTGRES_SUPERUSER @@ -138,6 +157,7 @@ mkdir -p \ "$HOST_BOTS_WORKSPACE_ROOT" docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" config -q +ensure_docker_network "$DOCKER_NETWORK_NAME" "$DOCKER_NETWORK_SUBNET" echo "[deploy-full] starting postgres and redis" docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d postgres redis diff --git a/scripts/deploy-prod.sh b/scripts/deploy-prod.sh index 9ecf6eb..1cdcc84 100755 --- a/scripts/deploy-prod.sh +++ b/scripts/deploy-prod.sh @@ -80,6 +80,23 @@ load_env_var() { printf -v "$name" '%s' "$value" } +ensure_docker_network() { + local network_name="$1" + local network_subnet="${2:-}" + + if docker network inspect "$network_name" >/dev/null 2>&1; then + echo "[deploy] reusing docker network: $network_name" + return 0 + fi + + echo "[deploy] creating docker network: $network_name" + if [[ -n "$network_subnet" ]]; then + docker network create --driver bridge --subnet "$network_subnet" "$network_name" >/dev/null + else + docker network create --driver bridge "$network_name" >/dev/null + fi +} + if [[ ! -f "$ENV_FILE" ]]; then echo "[WARNING] Missing env file: $ENV_FILE" echo "[WARNING] Creating it from: $ROOT_DIR/.env.prod.example ..." @@ -97,6 +114,8 @@ require_dir "$SKILLS_DIR" "Expected tracked skills directory under project-root load_env_var HOST_BOTS_WORKSPACE_ROOT load_env_var DATABASE_URL load_env_var NGINX_PORT 8080 +load_env_var DOCKER_NETWORK_NAME dashboard-nanobot-network +load_env_var DOCKER_NETWORK_SUBNET 172.20.0.0/16 load_env_var REDIS_ENABLED false load_env_var REDIS_URL @@ -121,6 +140,7 @@ mkdir -p "$DATA_DIR" "$DATA_DIR/model" "$HOST_BOTS_WORKSPACE_ROOT" echo "[deploy] expecting external PostgreSQL to be pre-initialized with scripts/sql/create-tables.sql and scripts/sql/init-data.sql" docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" config -q +ensure_docker_network "$DOCKER_NETWORK_NAME" "$DOCKER_NETWORK_SUBNET" docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d --build echo "[deploy] service status"