diff --git a/channel/weixin/weixin_channel.py b/channel/weixin/weixin_channel.py index 08ac4de0..03da3a7d 100644 --- a/channel/weixin/weixin_channel.py +++ b/channel/weixin/weixin_channel.py @@ -160,6 +160,30 @@ class WeixinChannel(ChatChannel): print(f"\n 二维码链接: {qrcode_url}") print(" (安装 'qrcode' 包可在终端显示二维码)\n") + def _notify_cloud_qrcode(self, qrcode_url: str): + """Send QR code URL to cloud console when running in cloud mode.""" + if not self.cloud_mode: + return + try: + from common import cloud_client + client = getattr(cloud_client, "chat_client", None) + if client and getattr(client, "client_id", None): + client.send_channel_qrcode("weixin", qrcode_url) + except Exception as e: + logger.warning(f"[Weixin] Failed to notify cloud QR code: {e}") + + def _notify_cloud_connected(self): + """Send connected status to cloud console when login succeeds.""" + if not self.cloud_mode: + return + try: + from common import cloud_client + client = getattr(cloud_client, "chat_client", None) + if client and getattr(client, "client_id", None): + client.send_channel_status("weixin", "connected") + except Exception as e: + logger.warning(f"[Weixin] Failed to notify cloud connected: {e}") + def _qr_login(self, base_url: str) -> dict: """Perform interactive QR code login. Returns dict with token/base_url or empty dict.""" api = WeixinApi(base_url=base_url) @@ -179,11 +203,12 @@ class WeixinChannel(ChatChannel): self._current_qr_url = qrcode_url logger.info(f"[Weixin] QR code URL: {qrcode_url}") self._print_qr(qrcode_url) + self._notify_cloud_qrcode(qrcode_url) print(" 等待扫码...\n") scanned_printed = False - while True: + while not self._stop_event.is_set(): try: status_resp = api.poll_qr_status(qrcode) except Exception as e: @@ -209,6 +234,7 @@ class WeixinChannel(ChatChannel): self._current_qr_url = qrcode_url logger.info(f"[Weixin] New QR code: {qrcode_url}") self._print_qr(qrcode_url) + self._notify_cloud_qrcode(qrcode_url) except Exception as e: logger.error(f"[Weixin] QR refresh failed: {e}") return {} @@ -225,6 +251,7 @@ class WeixinChannel(ChatChannel): self._current_qr_url = "" print(f"\n ✅ 微信登录成功!bot_id={bot_id}") logger.info(f"[Weixin] Login confirmed: bot_id={bot_id}") + self._notify_cloud_connected() creds = { "token": bot_token, @@ -237,9 +264,10 @@ class WeixinChannel(ChatChannel): return {"token": bot_token, "base_url": result_base_url} - time.sleep(1) + self._stop_event.wait(1) - logger.warning("[Weixin] QR login timed out") + logger.info("[Weixin] QR login cancelled by stop event") + self._current_qr_url = "" return {} # ── Long-poll loop ───────────────────────────────────────────────── diff --git a/common/cloud_client.py b/common/cloud_client.py index 064bca23..b31ed3fb 100644 --- a/common/cloud_client.py +++ b/common/cloud_client.py @@ -260,11 +260,27 @@ class CloudClient(LinkAIClient): self._remove_channel_type(local_config, channel_type) self._save_config_to_file(local_config) + if channel_type in ("weixin", "wx"): + self._remove_weixin_credentials() + if self.channel_mgr: threading.Thread( target=self._do_remove_channel, args=(channel_type,), daemon=True ).start() + @staticmethod + def _remove_weixin_credentials(): + """Remove the weixin token credentials file so next connect triggers QR login.""" + cred_path = os.path.expanduser( + conf().get("weixin_credentials_path", "~/.weixin_cow_credentials.json") + ) + try: + if os.path.exists(cred_path): + os.remove(cred_path) + logger.info(f"[CloudClient] Removed weixin credentials: {cred_path}") + except Exception as e: + logger.warning(f"[CloudClient] Failed to remove weixin credentials: {e}") + # ------------------------------------------------------------------ # channel credentials helpers # ------------------------------------------------------------------ @@ -351,12 +367,31 @@ class CloudClient(LinkAIClient): except Exception as e: logger.error(f"[CloudClient] Failed to remove channel '{channel_type}': {e}") + def send_channel_qrcode(self, channel_type: str, qrcode_url: str): + """Report QR code URL for a channel that requires scan-to-login.""" + if self.client_id: + from linkai.api.client.client import ClientMsgType + msg = self._build_package(ClientMsgType.CHANNEL_STATUS) + msg["data"]["channelType"] = channel_type + msg["data"]["status"] = "qrcode" + msg["data"]["qrcodeUrl"] = qrcode_url + self._send_package(msg) + logger.info(f"[CloudClient] Sent QR code status for '{channel_type}'") + def _report_channel_startup(self, channel_type: str): """Wait for channel startup result and report to cloud.""" ch = self.channel_mgr.get_channel(channel_type) if not ch: self.send_channel_status(channel_type, "error", "channel instance not found") return + + if channel_type in ("weixin", "wx") and hasattr(ch, "login_status"): + login_status = getattr(ch, "login_status", "") + if login_status in ("waiting_scan", "scanned", "idle"): + logger.info(f"[CloudClient] Channel '{channel_type}' is waiting for QR login, " + "skip reporting connected") + return + success, error = ch.wait_startup(timeout=3) if success: logger.info(f"[CloudClient] Channel '{channel_type}' connected, reporting status") diff --git a/docs/channels/weixin.mdx b/docs/channels/weixin.mdx index a0592cc3..11be7470 100644 --- a/docs/channels/weixin.mdx +++ b/docs/channels/weixin.mdx @@ -42,7 +42,7 @@ description: 将 CowAgent 接入个人微信 首次启动时,终端会显示一个二维码(有效期约 2 分钟)。使用微信扫描二维码并在手机上确认后即可完成登录。 - 二维码过期后会自动刷新并重新显示 -- 安装 `qrcode` Python 包可在终端直接渲染二维码图案(`pip3 install qrcode`) +- `requirements.txt` 中已默认包含 `qrcode` 依赖,安装后可在终端直接渲染二维码图案 ### 凭证保存 diff --git a/docs/en/channels/weixin.mdx b/docs/en/channels/weixin.mdx index 75e6c9d5..6cd90a45 100644 --- a/docs/en/channels/weixin.mdx +++ b/docs/en/channels/weixin.mdx @@ -42,7 +42,7 @@ Login credentials are automatically saved to `~/.weixin_cow_credentials.json`. T On first startup, a QR code is displayed in the terminal (valid for approximately 2 minutes). Scan it with WeChat and confirm on your phone. - The QR code automatically refreshes when it expires -- Install the `qrcode` Python package to render the QR code directly in the terminal: `pip3 install qrcode` +- The `qrcode` dependency is already included in `requirements.txt`, enabling QR code rendering directly in the terminal ### Credential Persistence diff --git a/docs/ja/channels/weixin.mdx b/docs/ja/channels/weixin.mdx index 346622a4..912cce24 100644 --- a/docs/ja/channels/weixin.mdx +++ b/docs/ja/channels/weixin.mdx @@ -42,7 +42,7 @@ description: CowAgent を個人の WeChat に接続する 初回起動時に、ターミナルに QR コードが表示されます(有効期限は約 2 分)。WeChat でスキャンし、スマートフォンで確認してください。 - QR コードが期限切れになると自動的に更新・再表示されます -- `qrcode` Python パッケージをインストールすると、ターミナルに直接 QR コードを表示できます:`pip3 install qrcode` +- `qrcode` 依存関係は `requirements.txt` にデフォルトで含まれており、ターミナルに直接 QR コードを表示できます ### 認証情報の永続化