feat(gateway): ✨ Add IM channel gateway for WeChat and WeCom#215
Conversation
Add a complete IM gateway system that enables AI agent interaction through WeChat (iLink Bot) and WeCom platforms, supporting 7×24 unattended operation. Key components: - Gateway supervisor for child process lifecycle management (start/stop/restart with PID tracking and version-aware auto-restart on upgrade) - Platform adapters for WeChat iLink and WeCom WebSocket - Command router for slash commands (/ws, /threads, /new, /resume, /stop, /status, /help) - Approval bridge routing tool approval requests through IM with Y/N response parsing - User session persistence binding IM users to workspace/thread - TOML-based gateway configuration with per-platform settings - Database migration for gateway_sessions table - Tauri IPC commands for frontend gateway control
- Add weixin_auth module with QR code request, login polling, and session persistence (~/.tiy/gateway/weixin/session.json) - Add 4 Tauri IPC commands: gateway_weixin_qr_login, gateway_weixin_login_poll, gateway_weixin_session, gateway_weixin_logout - Make WeixinConfig.token optional and account_id default-empty, resolving credentials from config > session file - Update WeixinAdapter to use resolved token/account_id, add AuthorizationType header, and align UIN encoding with hermes-agent - Add gateway channels documentation
…ackoff Enhance reliability of WeCom and WeChat adapters: - Add subscribe response verification in WeCom to detect auth failures early instead of silently proceeding - Add msg_id dedup with TTL in WeCom and content fingerprint dedup in WeChat to prevent duplicate message processing - Differentiate error backoff in WeChat: long backoff (600s) for session expiration, multiplied backoff for rate limiting, and exponential backoff for generic errors - Fix context_token lookup to use composite key (account_id:peer_id) for proper multi-account support - Switch WeCom outbound msg_type from text to markdown and truncate messages exceeding 4000 characters
Show PID, version, and config status in gateway settings panel. Disable start button when config file is missing and display warning message with expected config path.
Add gateway configuration CRUD with frontend settings panel, and align WeCom/WeChat platform implementations with their respective protocols. - Add save/load config with atomic TOML writes - Add GatewayConfigDto and GatewayConfigUpdateInput for frontend IPC - Add enabled toggle to WeixinConfig and WecomConfig - Register gateway_get_config and gateway_save_config commands - Refactor WeCom WebSocket to use cmd/headers/body frame format - Add comprehensive message extraction (mixed, voice, appmsg, quotes) - Implement split-aware debounce for long message aggregation - Add stable device_id across WeCom reconnects - Switch WeChat auth QR endpoint from POST to GET with proper constants - Generate QR codes locally using qrcode crate (SVG output) - Add ScannedRedirect status for host redirection flow - Store base_url and user_id in WeixinSession - Remove hermes-agent references, rebrand to tiycode
Gateway now starts unconditionally and watches the config file for changes, dynamically reloading adapters when channel configuration is updated instead of requiring a restart. - Gateway runner polls config file every 5s and reloads on mtime change - Idles gracefully when no config exists or no channels are enabled - Uses tokio::select! to detect config changes during message loop - Removes config existence check from supervisor startup - Gateway::run() now accepts config_path instead of pre-loaded config UI changes: - Extract shared settings components (SettingsSection, SettingsRow, PageHeading, ChoiceGroup, SectionDivider) into settings-shared.tsx - Gateway panel auto-saves on channel toggle instead of manual save - Auto-enables WeChat channel on successful QR login - Compact status display using shared SettingsRow component
- Remove stop button, keep restart as the sole control - Simplify platform names (remove bot suffixes) - Clarify status label as "Gateway Status"
…r handling - Restructure request body with `base_info` and `get_updates_buf` fields - Parse response fields `msgs` and `get_updates_buf` instead of `updates` and `sync_buf` - Check `ret` field alongside `errcode` for more robust error detection - Add diagnostic logging for request URLs and response summaries - Skip persisting empty sync buffer values
Replace stable client_id with per-message client_id to comply with iLink requirements. Defer welcome message until first inbound message to obtain the correct reply target (chat_id instead of session.user_id). Update message parsing to handle iLink's nested text content and numeric message_id. Use from_user_id for DM responses and group_id for group responses. Add detailed logging to aid debugging of message flow.
…se_info per protocol
…g back to global default Aligns gateway behavior with ACP: first checks thread.profile_id, only falls back to resolve_active_profile_id when thread has none.
… switching profiles - /profile lists all agent profiles with current thread's profile marked ★ - /profile <N> switches the current thread to use that profile - Consistent with /ws and /threads command patterns
- Add /profile and /profile <N> to /help command list - Fix getconfig: omit context_token when unavailable instead of sending empty string - Change typing_ticket cache from global singleton to per-user HashMap - Add warn logging for getconfig failures - Clear ticket cache per-user on sendtyping failure
…fallthrough In the match arm for unexpected events, the lack of a continue statement allowed execution to fall through to the subsequent approval handling logic, potentially causing incorrect behavior. Adding the continue ensures the loop correctly skips processing for unhandled events.
…download and AES encryption Implement multimedia messaging for the WeChat platform, enabling the gateway to send and receive images, voice, files, and videos via the WeChat CDN. - Add `weixin_media` module with types for CDN media items, inbound attachment extraction, outbound upload flow, and AES-256-CBC encryption per WeChat protocol. - Extend `PlatformAdapter` trait with `send_media` method for optional platform-specific media upload. - Implement `send_media` on `WeixinAdapter` to encrypt and upload files to CDN, then send structured media items. - Parse inbound media attachments from message `item_list` and pass them to the agent for context-aware processing. - Skip text‑only dedup for slash commands to allow repeated command invocations. - Add necessary dependencies (aes, cbc, rand, md-5, hex) for encryption and hashing.
…simplify auth Align the send_media request structure with the iLink protocol, which requires an envelope containing base_info and msg fields. This ensures compatibility with the updated API endpoint. Additionally, replace the custom BearerTokenExt trait with reqwest's built-in bearer_auth method for cleaner and more maintainable code.
…ages for LLM vision Previously, the agent could not "hear" voice messages because transcriptions were not included in the prompt, and images encrypted with AES-256-CBC were passed as raw URLs, preventing the LLM from seeing the content. This commit adds two capabilities: - Voice transcriptions from media attachments are merged into the prompt, allowing the agent to process voice inputs. - For WeChat images containing an AES key, the encrypted payload is downloaded from the CDN, decrypted, and converted to a base64 `data:` URL, enabling the LLM to visually analyze the image. These changes improve multimodal support and make the agent responsive to voice and image inputs in WeChat gateway conversations.
…t caching - Implement stop_typing by sending status=2 (cancel) via sendtyping API - Add 24-hour TTL for cached typing tickets to force refresh on expiry - Fix iLink API response parsing to check `ret` field before `errcode` - Skip ticket fetch on cancel when no cached ticket exists
WeCom outbound sends now register a pending ack keyed by req_id and await the server's response frame with a timeout, validating errcode before reporting success. This replaces fire-and-forget sending with confirmed delivery, catching server-side rejections and transport loss early. The reader loop resolves pending acks before normal message parsing, and stale waiters are cleared on disconnect. Reconnect backoff is lifted from the WeCom adapter into the gateway runner so all adapters benefit from a consistent progressive delay schedule (2s → 5s → 10s → 30s → 60s) instead of fixed sleeps after disconnects or failures. The attempt counter resets on clean config-driven reloads.
Add typing indicator signals at additional interaction points: before number selection handling and command dispatch, giving users immediate feedback while processing begins. Improve WeChat typing ticket robustness and observability: - Validate typing_ticket is non-empty after getconfig - Add structured context (has_context_token, status) to log fields - Upgrade log levels from debug to warn/info for typing failures and successes to aid production troubleshooting
AI Code Review SummaryPR: #215 (feat(gateway): ✨ Add IM channel gateway for WeChat and WeCom) Overall AssessmentDetected 16 actionable findings, prioritize CRITICAL/HIGH before merge. Major Findings by Severity
Actionable Suggestions
Potential Risks
Test Suggestions
File-Level Coverage Notes
Inline Downgraded Items (processed but not inline)
Coverage Status
Uncovered list:
No-patch covered list:
Runtime/Budget
|
| use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream}; | ||
|
|
||
| use crate::gateway::config::WecomConfig; | ||
| use crate::gateway::traits::{InboundMessage, Platform, PlatformAdapter, SendResult}; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| DialogTitle, | ||
| } from "@/shared/ui/dialog"; | ||
| import { AgentsSettingsPanel } from "@/modules/settings-center/ui/agents-settings-panel"; | ||
| import { GatewaySettingsPanel } from "@/modules/settings-center/ui/gateway-settings-panel"; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| use tauri::State; | ||
|
|
||
| use crate::core::gateway_supervisor::{GatewayStatus, GatewaySupervisorHandle}; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| GatewayCommand::Stop => { | ||
| if let SessionState::AgentRunning { .. } = session.state { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| && att.aes_key.is_some() | ||
| { | ||
| // Download + decrypt image → data: URL for LLM vision | ||
| match crate::gateway::platforms::weixin_media::download_media_as_data_url( |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| let child = Command::new(&exe) | ||
| .args([ | ||
| "gateway", | ||
| "--config", |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| let mut headers = reqwest::header::HeaderMap::new(); | ||
| headers.insert( | ||
| "Authorization", | ||
| format!("Bearer {}", *token).parse().unwrap(), |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| /// Build the standard iLink API headers. | ||
| async fn api_headers(&self) -> reqwest::header::HeaderMap { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| }; | ||
|
|
||
| const pollLogin = async (uuid: string) => { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| /// Build the full API URL for an endpoint. | ||
| fn api_url(&self, endpoint: &str) -> String { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| @@ -0,0 +1,429 @@ | |||
| /** | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// Called by the frontend after the user saves gateway configuration. | ||
| #[tauri::command] | ||
| pub async fn gateway_start(supervisor: State<'_, GatewaySupervisorHandle>) -> Result<bool, String> { | ||
| supervisor.ensure_started().await.map_err(|e| e.to_string()) |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| }) | ||
| } | ||
|
|
||
| pub fn run_gateway(config_path: Option<&str>) -> anyhow::Result<()> { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| }; | ||
|
|
||
| const pollLogin = async (uuid: string) => { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| aes = "0.8" | ||
| cbc = { version = "0.1", features = ["alloc"] } | ||
| rand = "0.8" | ||
| md-5 = "0.10" |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| tracing::info!(elapsed_ms = recovery_start.elapsed().as_millis(), "⏱ [startup-recovery] total"); | ||
| }); | ||
|
|
||
| // 7. Auto-start gateway if config exists (or restart on version upgrade). |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| // 7. Auto-start gateway if config exists (or restart on version upgrade). | ||
| let gateway_handle = app.handle().clone(); | ||
| tauri::async_runtime::spawn(async move { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| wecomWsUrl: string; | ||
| }>) => { | ||
| try { | ||
| await invoke("gateway_save_config", { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| }; | ||
|
|
||
| const pollLogin = async (uuid: string) => { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| [target.'cfg(target_os = "windows")'.dependencies] | ||
| winreg = "0.55" | ||
| windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_System_Threading"] } |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
…am with single I/O task Previously, the WebSocket stream was split into a separate sink and reader, with the sink shared via Arc<Mutex>. Multiple tasks competing for the sink lock caused cross-task TLS stream contention that stalled inbound reads. This commit replaces the split-stream pattern with a single I/O task that solely owns ws_stream and multiplexes reads, outbound writes (via mpsc channel), and connection-loss signals through one select!. Changes: - Replace WsSink with outbound channel; all writes go through mpsc - Remove dual-layer keepalive (WS Ping + JSON ping); use only application-level cmd:"ping" since server doesn't reliably answer protocol-level Ping frames - Track liveness on any inbound frame, not just protocol Pong - Perform handshake inline on unsplit stream before spawning I/O task - Use connect_async_with_config with explicit standard RFC 6455 settings - Simplify disconnect by dropping outbound sender instead of sink.close() Fixes connection deadlocks where pending sends blocked the reader task from processing inbound frames and heartbeat pong timeouts.
| // Main message loop with config change detection. | ||
| // Re-snapshot mtime right before entering the loop to avoid false positives | ||
| // from the outer loop's read vs inner loop's first check. | ||
| *last_mtime = file_mtime(config_path); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| } | ||
|
|
||
| #[async_trait::async_trait] |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// Response contains `qrcode` (hex token for polling) and `qrcode_img_content` | ||
| /// (full scannable liteapp URL). We generate a QR code SVG locally from the | ||
| /// scannable URL and return it as base64 for the frontend to display. | ||
| pub async fn request_qr_code(base_url: Option<&str>) -> anyhow::Result<QrLoginResult> { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// Download media from WeChat CDN and decrypt it. | ||
| /// | ||
| /// Returns the decrypted plaintext bytes. | ||
| pub async fn download_and_decrypt_media( |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| wecomSecret: string; | ||
| wecomWsUrl: string; | ||
| }>) => { | ||
| try { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| /// Parse a single update JSON into an InboundMessage (with dedup). | ||
| fn parse_update( |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| app.manage(state); | ||
| app.manage(desktop_runtime); | ||
| app.manage(crate::core::gateway_supervisor::GatewaySupervisorHandle::new()); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| tracing::info!(elapsed_ms = recovery_start.elapsed().as_millis(), "⏱ [startup-recovery] total"); | ||
| }); | ||
|
|
||
| // 7. Auto-start gateway if config exists (or restart on version upgrade). |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| }; | ||
|
|
||
| const pollLogin = async (uuid: string) => { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| commands::terminal::terminal_close, | ||
| commands::terminal::terminal_list, | ||
| commands::terminal::terminal_list_available_shells, | ||
| // Gateway |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| //! directly (bypassing ACP) via a command-routing + event-pump architecture. | ||
|
|
||
| pub mod approval_bridge; | ||
| pub mod command_router; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| @@ -0,0 +1,312 @@ | |||
| //! Gateway process supervisor — manages the lifecycle of the IM gateway child process. | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| .strip_prefix("/ws add ") | ||
| .or_else(|| trimmed.strip_prefix("/ws add\t")) | ||
| { | ||
| let parts: Vec<&str> = rest.splitn(2, ' ').collect(); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| // Send deferred welcome on first inbound message. | ||
| if !welcome_sent { | ||
| welcome_sent = true; | ||
| let welcome = "👋 你好!我是 TiyCode AI 助手\n\n\ |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// - Markdown tables → key-value list | ||
| /// - Long lines (>120 chars) → wrapped | ||
| /// - Code blocks preserved as-is | ||
| pub fn normalize_markdown_for_im(text: &str) -> String { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| wecomWsUrl: string; | ||
| }>) => { | ||
| try { | ||
| await invoke("gateway_save_config", { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| @@ -0,0 +1,258 @@ | |||
| //! Gateway configuration structures and TOML loading. | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| }; | ||
|
|
||
| const pollLogin = async (uuid: string) => { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| use std::time::{Duration, SystemTime}; | ||
|
|
||
| use futures::StreamExt; | ||
| use tokio::sync::{broadcast, mpsc}; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| /// Establish WebSocket connection. | ||
| async fn connect_ws(&self) -> anyhow::Result<WsStream> { | ||
| let url = format!("wss://{}/", self.config.ws_url); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
- Clear session on errcode -14 during send instead of silently retrying - Clear session and stop polling on session_expired instead of long backoff - Detect session expiry in UI when session drops while gateway is running - Show amber "Session expired" status with prompt to re-scan QR code - Add i18n keys for session expiration messages in en and zh-CN
| /// The gateway starts unconditionally and watches `config_path` for changes. | ||
| /// When no config exists or no channels are enabled, it idles and polls. | ||
| /// When config changes are detected, adapters are reloaded. | ||
| pub async fn run(state: GatewayState, config_path: PathBuf) -> anyhow::Result<()> { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// Merges the input into the existing config (or creates a new one). | ||
| /// Empty `wecom_secret` means "keep the existing secret". | ||
| #[tauri::command] | ||
| pub async fn gateway_save_config(input: GatewayConfigUpdateInput) -> Result<(), String> { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// Frontend should display the returned QR image/URL, then poll with | ||
| /// `gateway_weixin_login_poll` until login completes. | ||
| #[tauri::command] | ||
| pub async fn gateway_weixin_qr_login(base_url: Option<String>) -> Result<QrLoginResult, String> { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
|
|
||
| impl GatewayConfig { | ||
| /// Load configuration from a TOML file. | ||
| pub fn load(path: &Path) -> anyhow::Result<Self> { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| /// Save a gateway config to the default TOML path atomically. | ||
| pub fn save_config(config: &GatewayConfig) -> anyhow::Result<()> { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| description={ | ||
| status?.running | ||
| ? `PID ${status.pid ?? "—"} · ${status.version ? `v${status.version}` : "—"} · Config ${status.configExists ? "✓" : "✗"}` | ||
| : "Gateway process is not running." |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| ".fleet", | ||
| "DerivedData", | ||
| ".build", | ||
| "cmake-build-", |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| * Gateway/Channels settings panel — WeChat & WeCom channel configuration. | ||
| */ | ||
| import { useCallback, useEffect, useRef, useState } from "react"; | ||
| import { invoke, isTauri } from "@tauri-apps/api/core"; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| weixinEnabled, | ||
| wecomEnabled, | ||
| wecomBotId, | ||
| wecomSecret: "", |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } from "@/shared/ui/dialog"; | ||
| import { AgentsSettingsPanel } from "@/modules/settings-center/ui/agents-settings-panel"; | ||
| import { GatewaySettingsPanel } from "@/modules/settings-center/ui/gateway-settings-panel"; | ||
| import { PageHeading, SettingsSection, SettingsRow, ChoiceGroup, SectionDivider } from "@/modules/settings-center/ui/settings-shared"; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
移除飞书和 WhatsApp 两个 IM 频道的全部代码,包括: - Rust 后端:删除 feishu.rs (1218行) 和 whatsapp.rs (603行) 适配器 - 配置模型:移除 GroupPolicy/DmPolicy/FeishuConfig/WhatsAppConfig - 平台枚举:从 Platform 移除 Feishu 和 WhatsApp 变体 - IPC 命令:移除配置保存中的对应逻辑 - 前端设置面板:移除两个频道的 UI 配置区块和状态变量 - i18n:移除中英文翻译键值(各 17 条) 暂时不提供这两个频道,保留微信和企业微信频道不变。
Add comprehensive validation for WeChat and WeCom gateway configs: - Check for missing platform sections and empty required fields - Warn if WeChat token is missing (QR login fallback) - Fail early if WeCom bot_id or secret are empty when channel is enabled Improve directory exclusion logic in file indexing: - Support prefix-based matching for patterns like "cmake-build-" - Use new is_dir_blocked helper for consistency Add i18n support for gateway settings UI: - New English and Chinese translations for QR login, scanning, loading states - Replace hardcoded strings with localized ones in gateway-settings-panel - Improve UX by displaying translated error messages on save and QR login failures Restrict config file permissions on Unix to 600 after save.
| @@ -0,0 +1,1274 @@ | |||
| //! Gateway runner — main event loop that drives the IM platform adapter, | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| } | ||
|
|
||
| fn safely_build_and_replace_compressed_messages( |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| path: path.clone(), | ||
| name, | ||
| }; | ||
| match state.workspace_manager.add(input).await { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| @@ -0,0 +1,329 @@ | |||
| //! Message formatting and chunking for IM platforms — placeholder for Step 4. | |||
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| /// Persist session to disk atomically. | ||
| pub fn save_session(session: &WeixinSession) -> anyhow::Result<()> { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
| }; | ||
|
|
||
| const pollLogin = async (uuid: string) => { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| /// Frontend should display the returned QR image/URL, then poll with | ||
| /// `gateway_weixin_login_poll` until login completes. | ||
| #[tauri::command] | ||
| pub async fn gateway_weixin_qr_login(base_url: Option<String>) -> Result<QrLoginResult, String> { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| } | ||
|
|
||
| async fn expire_pending_plan_approval(&self, thread_id: &str) -> Result<(), AppError> { | ||
| pub async fn expire_pending_plan_approval(&self, thread_id: &str) -> Result<(), AppError> { |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| .unwrap_or_default() | ||
| } | ||
|
|
||
| /// Check whether a directory name should be excluded or lazy-loaded. |
There was a problem hiding this comment.
[MEDIUM] No tests for is_dir_blocked with prefix matching
The directory filtering logic used in manifest collection and preloading is untested, risking incorrect exclusion or preloading of directories.
Suggestion: Add unit tests for is_dir_blocked with exact match, prefix match, and no-match cases, including edge cases like empty name or pattern.
Risk: Build directories like 'cmake-build-debug' could be incorrectly included in the file manifest or preloaded, causing performance or correctness issues.
Confidence: 0.90
| "send rejected: errcode={errcode} errmsg={errmsg}" | ||
| ))) | ||
| } | ||
| } |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
…ncel stale QR polls
| } | ||
| } | ||
|
|
||
| impl Drop for GatewaySupervisor { |
There was a problem hiding this comment.
[HIGH] Child process not awaited on drop; zombie gateway on GUI crash if supervisor field is lost
By design, the gateway child is not killed on drop to support 7×24 operation. However, the owned Child handle will be dropped without being waited on, which is safe on Unix but may leak a process handle on Windows.
Suggestion: Ensure the Child is 'detached' before drop to avoid orphaned process handles on Windows, or document the Windows handle-leaking behavior and its low impact.
Risk: Minor resource leak on Windows when the GUI process exits unexpectedly.
Confidence: 0.85
| } | ||
|
|
||
| /// Core runner logic extracted for testability. | ||
| async fn run_with_adapter( |
There was a problem hiding this comment.
[HIGH] GatewayRunner run_with_adapter has no test coverage
The extracted run_with_adapter function—marked as extracted for testability—has zero unit or integration tests.
Suggestion: Add at least integration tests for run_with_adapter using a mock PlatformAdapter and a deterministic GatewayState. Cover the config-change exit, adapter-disconnect exit, welcome-send path, approval-response routing, and stop-signal handling.
Risk: Regressions in critical gateway event-loop logic go undetected; all testing relies on end-to-end manual IM interactions.
Confidence: 0.92
| // directory. Agent-initiated workspace adds through the ACP | ||
| // server follow a different code path. | ||
| let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); | ||
| let resolved = dunce::canonicalize(&path) |
There was a problem hiding this comment.
[HIGH] Workspace addition skips canonicalization when path does not exist
Workspace addition via IM commands fails for non-existent paths, even though the workspace manager supports adding non-existent directories (they will be created later). This breaks the /ws add flow for new project paths.
Suggestion: Check if the path exists before canonicalizing; if it doesn't, resolve parent directories instead or use a different validation strategy (e.g., only validate path structure, not existence).
Risk: Users cannot add new workspace directories that don't exist yet, rendering the IM workspace creation feature unusable for typical workflows.
Confidence: 0.90
| /// | ||
| /// This is factored out so it can be reused both for a fresh `start_run` and | ||
| /// for `execute_approved_plan` (which also returns an event receiver). | ||
| async fn run_event_pump( |
There was a problem hiding this comment.
[HIGH] Gateway event pump and approval timeout flow are untested
The run_event_pump function—which drives the entire agent response flow including approvals, plans, clarification, and cancellations—is entirely untested.
Suggestion: Write integration tests with a mock broadcast sender and mpsc channels to exercise: tool approval approve/deny/timeout, clarification response/timeout, /stop during run, plan approval, RunCompleted, RunFailed, RunCancelled, and event receiver lagged/closed.
Risk: The IM approval loop is the primary interaction surface for end users; bugs here manifest as hangs, double-approvals, or dropped messages.
Confidence: 0.85
| /// When the heartbeat fails to enqueue, or no inbound activity is seen within | ||
| /// `PONG_TIMEOUT`, the connection is considered dead and a reconnect is | ||
| /// signalled via `connection_lost_tx`. | ||
| fn start_heartbeat( |
There was a problem hiding this comment.
[HIGH] WeComAdapter WebSocket lifecycle and heartbeat are untested
The WeComAdapter's WebSocket lifecycle, heartbeat liveness detection, ack resolution, and message extraction have no automated tests.
Suggestion: Write integration tests with a local WebSocket echo server or mock WsStream to test: connect/subscribe, ping/pong heartbeat, connection-lost signal, send+ack resolution, and inbound message parsing for mixed/voice/appmsg types.
Risk: Heartbeat failures or ack resolution bugs could cause the WeCom gateway to silently drop messages or fail to reconnect.
Confidence: 0.85
| ...overrides, | ||
| }, | ||
| }); | ||
| setConfigDirty(false); |
There was a problem hiding this comment.
[HIGH] Auto-save toggle switches discard unsaved secret/botId changes
When a user modifies wecomBotId or wecomSecret, the config becomes dirty. If the user then toggles a channel switch (WeChat or WeCom), autoSaveConfig saves the toggle but clears the dirty flag, causing any typed bot ID or secret to be silently dropped without a manual save opportunity.
Suggestion: Do not call setConfigDirty(false) in autoSaveConfig. Only clear the dirty flag when all pending changes have been persisted (e.g., after explicit Save). The Save button should remain visible until the user manually saves secrets.
Risk: Users may lose critical configuration data (e.g., Bot ID, secrets) after toggling a channel, leading to authentication failures and a confusing experience.
Confidence: 0.90
|
|
||
| /// Validate that the required platform config section is present and | ||
| /// that required credentials are non-empty for enabled channels. | ||
| fn validate(&self) -> anyhow::Result<()> { |
There was a problem hiding this comment.
[MEDIUM] GatewayConfig validation and DTO mapping are untested
Config validation logic and the DTO mapping layer lack tests for edge cases and default behaviors.
Suggestion: Add tests for GatewayConfig::validate covering both platforms, missing sections, empty/invalid credentials, and disabled channels. Add tests for GatewayConfigDto::from_config covering all Option variants.
Risk: Invalid or partial configs may pass silently, and frontend DTO serialization could be incorrect for new fields.
Confidence: 0.90
| /// Compute the reconnect delay for a given consecutive-attempt index using the | ||
| /// fixed backoff schedule. The index is clamped to the final entry so sustained | ||
| /// outages settle at the longest interval instead of overflowing. | ||
| fn reconnect_backoff_delay(attempt: usize) -> Duration { |
There was a problem hiding this comment.
[MEDIUM] Reconnect backoff schedule has no test for boundary behavior
The reconnect backoff delay function lacks tests for clamp boundary and exact schedule values.
Suggestion: Add unit tests for reconnect_backoff_delay with attempt=0, attempt=4, attempt=5, and attempt=100 to verify correct clamping and values.
Risk: An off-by-one in backoff could cause excessive reconnect rate or unnecessarily long idle periods.
Confidence: 0.88
| setConfigDirty(false); | ||
| }, []); | ||
|
|
||
| const autoSaveConfig = useCallback(async (overrides: Partial<{ |
There was a problem hiding this comment.
[MEDIUM] Auto-save function uses stale state via closure
autoSaveConfig may send stale config values when invoked shortly after another config toggle, risking malformed or unintended config saves.
Suggestion: Write test cases that simulate rapid toggle sequences (e.g., toggle weixinEnabled then immediately toggle wecomEnabled) and assert the payload sent to gateway_save_config matches the expected final state. Use ref-based latest state or functional setState to mitigate the stale-closure issue if confirmed by tests.
Risk: Stale state in auto-save can cause unintended channel enable/disable states to be persisted to the backend, potentially leaving WeChat or WeCom in a wrong state after quick user interactions.
Confidence: 0.85
| {weixinSession ? ( | ||
| <SettingsRow | ||
| label={weixinSession.account_id || "—"} | ||
| description={`Token: ${(weixinSession.token ?? "").slice(0, 8)}…`} |
There was a problem hiding this comment.
[MEDIUM] Weixin session token rendered in settings UI
The Weixin session token prefix is displayed in cleartext in the settings UI, which could be exposed via screen-sharing, screenshots, or if a malicious extension inspects the DOM.
Suggestion: Obfuscate or avoid displaying any part of the token. If it must be shown for debugging, place it behind an explicit reveal action and consider using a masked-only indicator.
Risk: Medium. Requires physical access, screen sharing, or a compromised renderer to exploit, but session tokens are high-value targets.
Confidence: 0.85
Summary
PlatformAdaptertrait with shared message types (InboundMessage,SendResult), command routing, approval bridging, user sessions, message formatting, and config management with dynamic watchingTest Plan
npm run typecheck— no new errorscargo test --locked --manifest-path src-tauri/Cargo.toml— all tests pass🤖 Generated with TiyCode