From b8bd9b459a8a9f981ccce74a296f34f8d25dac44 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Wed, 12 Aug 2026 10:10:37 +0800 Subject: [PATCH] fix(startup): keep the app bootable when config is partially broken --- agent/registry.py | 28 ++++++++++++++++++++++- app.py | 13 ++++++++++- channel/wechatcom/wechatcomapp_channel.py | 19 +++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/agent/registry.py b/agent/registry.py index 4474e7e8..a729b357 100644 --- a/agent/registry.py +++ b/agent/registry.py @@ -280,6 +280,8 @@ def get_agent_registry() -> AgentRegistry: """ global _registry_instance, _registry_signature + import os + from config import conf settings = conf() @@ -288,7 +290,31 @@ def get_agent_registry() -> AgentRegistry: if _registry_pinned and _registry_instance is not None: return _registry_instance if _registry_instance is None or _registry_signature != signature: - _registry_instance = AgentRegistry.from_config(settings) + try: + _registry_instance = AgentRegistry.from_config(settings) + except AgentRegistryError: + # An invalid `agents`/`default_agent_id` block would otherwise + # bubble all the way up through load_config() and take the whole + # process down before the web console can bind — leaving a + # desktop user with no UI to fix the very config that is broken. + # Fall back to the default single agent so the app still starts; + # the console can then edit the bad block. Source deployments + # keep failing loudly so the developer sees the error at once. + if os.environ.get("COW_DESKTOP") != "1": + raise + from common.log import logger + + logger.error( + "[AgentRegistry] invalid 'agents' config; ignoring it and " + "starting with the default agent. Fix it in the console.", + exc_info=True, + ) + fallback = { + k: v + for k, v in dict(settings).items() + if k not in ("agents", "default_agent_id") + } + _registry_instance = AgentRegistry.from_config(fallback) _registry_signature = signature return _registry_instance diff --git a/app.py b/app.py index 723aff7c..24e86c3b 100644 --- a/app.py +++ b/app.py @@ -82,7 +82,18 @@ class ChannelManager: with self._lock: channels = [] for name in channel_names: - ch = channel_factory.create_channel(name) + # One misconfigured channel (e.g. wechatcom_app without its + # corp_id/token/aes_key) must not take the whole process down: + # instantiating it can raise while parsing config. The web + # console in particular has to come up so the desktop shell can + # surface the error and let the user fix the config. Skip the + # broken channel and keep the rest. + try: + ch = channel_factory.create_channel(name) + except Exception as e: + logger.error(f"[ChannelManager] Failed to create channel '{name}', skipping it: {e}") + logger.exception(e) + continue ch.cloud_mode = self.cloud_mode self._channels[name] = ch channels.append((name, ch)) diff --git a/channel/wechatcom/wechatcomapp_channel.py b/channel/wechatcom/wechatcomapp_channel.py index 3321f541..9717612d 100644 --- a/channel/wechatcom/wechatcomapp_channel.py +++ b/channel/wechatcom/wechatcomapp_channel.py @@ -40,6 +40,25 @@ class WechatComAppChannel(ChatChannel): logger.info( "[wechatcom] Initializing WeCom app channel, corp_id: {}, agent_id: {}".format(self.corp_id, self.agent_id) ) + # Fail fast with a readable message when the channel is enabled but its + # required credentials are missing. Otherwise WeChatCrypto concatenates + # a None aes_key with a string and raises an opaque + # "unsupported operand type(s) for +: 'NoneType' and 'str'". + missing = [ + key + for key, val in ( + ("wechatcom_corp_id", self.corp_id), + ("wechatcomapp_token", self.token), + ("wechatcomapp_aes_key", self.aes_key), + ) + if not val + ] + if missing: + raise RuntimeError( + "[wechatcom] WeCom app channel is enabled but missing required config: " + + ", ".join(missing) + + ". Fill them in config.json or remove 'wechatcom_app' from channel_type." + ) self.crypto = WeChatCrypto(self.token, self.aes_key, self.corp_id) self.client = WechatComAppClient(self.corp_id, self.secret)