Skip to content

feat(gateway): ✨ Add IM channel gateway for WeChat and WeCom#215

Merged
jorben merged 49 commits into
masterfrom
feat/channel-gateway
May 29, 2026
Merged

feat(gateway): ✨ Add IM channel gateway for WeChat and WeCom#215
jorben merged 49 commits into
masterfrom
feat/channel-gateway

Conversation

@jorben
Copy link
Copy Markdown
Contributor

@jorben jorben commented May 28, 2026

Summary

  • Add IM channel gateway subsystem supporting WeChat (iLink) and WeCom (AI Bot WebSocket) platforms, enabling direct agent interaction via messaging channels
  • Implement gateway process supervisor for lifecycle management (start/stop/restart/auto-start on app launch, version upgrade restart), with PID tracking and cross-platform process control
  • Add gateway settings panel in Settings Center with WeChat QR login flow, WeCom credential configuration, and process status display
  • Introduce PlatformAdapter trait with shared message types (InboundMessage, SendResult), command routing, approval bridging, user sessions, message formatting, and config management with dynamic watching
  • Handle message deduplication, auth verification, exponential error backoff, and long-poll lifecycle tracing

Test Plan

  • Verify gateway auto-starts when config exists (or restarts on version upgrade)
  • Test WeChat QR login flow: request QR → scan → poll → session saved
  • Test WeCom enable/disable toggle with Bot ID and Secret fields
  • Verify gateway start/stop/restart from Settings UI
  • Confirm config changes are persisted and secrets are masked in get_config
  • Run npm run typecheck — no new errors
  • Run cargo test --locked --manifest-path src-tauri/Cargo.toml — all tests pass

🤖 Generated with TiyCode

jorben added 30 commits May 28, 2026 17:07
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.
…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
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 28, 2026

AI Code Review Summary

PR: #215 (feat(gateway): ✨ Add IM channel gateway for WeChat and WeCom)
Preferred language: English

Overall Assessment

Detected 16 actionable findings, prioritize CRITICAL/HIGH before merge.

Major Findings by Severity

  • CRITICAL (1)
    • src-tauri/src/core/gateway_supervisor.rs - Missing tests for GatewaySupervisor lifecycle and process utilities
  • HIGH (9)
    • src-tauri/src/core/agent_session_compression.rs - No test for panic recovery in compression post-persist
    • src-tauri/src/core/gateway_supervisor.rs:230 - Child process not awaited on drop; zombie gateway on GUI crash if supervisor field is lost
    • src-tauri/src/gateway/gateway_runner.rs:181 - GatewayRunner run_with_adapter has no test coverage
    • src-tauri/src/gateway/gateway_runner.rs:651 - Workspace addition skips canonicalization when path does not exist
    • src-tauri/src/gateway/gateway_runner.rs:996 - Gateway event pump and approval timeout flow are untested
    • src-tauri/src/gateway/platforms/wecom.rs:198 - WeComAdapter WebSocket lifecycle and heartbeat are untested
    • src-tauri/src/gateway/user_session.rs:122 - UserSession load_or_create and save have no test coverage
    • src/modules/settings-center/ui/gateway-settings-panel.tsx - No unit tests for new GatewaySettingsPanel logic
  • MEDIUM (5)
    • src-tauri/src/core/index_manager.rs:1148 - Missing tests for is_dir_blocked prefix matching
    • src-tauri/src/gateway/config.rs:50 - GatewayConfig validation and DTO mapping are untested
    • src-tauri/src/gateway/gateway_runner.rs:169 - Reconnect backoff schedule has no test for boundary behavior
    • src/modules/settings-center/ui/gateway-settings-panel.tsx:113 - Auto-save function uses stale state via closure
    • src/modules/settings-center/ui/gateway-settings-panel.tsx:369 - Weixin session token rendered in settings UI
  • LOW (1)
    • src-tauri/src/commands/gateway.rs - No tests for gateway Tauri commands

Actionable Suggestions

  • Fix the workspace addition command to tolerate non-existent paths (e.g., use parent resolution or skip canonicalize when path is not present).
  • Implement proper synchronization for plan approval state transitions to avoid race conditions.
  • Consider bounding the context token map in Weixin adapter with an LRU cache.
  • Add hostname validation for the redirect_host in the WeChat QR login to prevent SSRF.
  • Consider redacting message content in info-level logs or raising the log level for message text.
  • Validate config URLs (e.g., require HTTPS, restrict schemes) to reduce misconfiguration risks.
  • Add integration tests for run_with_adapter with a mock PlatformAdapter to cover the main event loop and approval routing.
  • Add unit tests for GatewayConfig::validate covering missing sections, empty credentials, and disabled channels.
  • Add integration tests for UserSession load/save/switch with an in-memory SQLite database.
  • Add unit tests for reconnect_backoff_delay boundary values.
  • Extract and test WeixinAdapter parse_update dedup logic (TTL expiration, content dedup, command bypass).
  • Add unit tests for WeComAdapter extract_text for mixed, voice, and appmsg message types.

Potential Risks

  • High-severity: Non-existent workspace paths block user onboarding via IM.
  • Medium: Race condition in plan approval may lead to missed approvals under concurrency.
  • Low: Unbounded resource usage in long-running adapter processes.
  • If the config file permissions are relaxed, the WeChat and WeCom adapters could be redirected to attacker-controlled servers with full credentials.
  • Unvalidated user input in the QR login redirect could be used to scan internal networks if the iLink API is compromised.
  • The entire gateway event loop is untested; regressions in approval timeout, plan approval, and clarification flows will not be caught by CI.
  • Config validation errors (missing sections, empty credentials) are not tested and could lead to runtime panics or silent misbehavior.
  • Session persistence layer is untested; data corruption or stale state could cause user confusion on gateway restart.
  • WeChat deduplication logic could drop legitimate repeated commands or fail to deduplicate duplicate messages.
  • WeCom heartbeat liveness detection is untested; undetected dead connections could lead to message black holes.
  • The gateway log file can grow indefinitely after initial truncation.
  • Windows process handle may be leaked if the supervisor is dropped without stop().

Test Suggestions

  • Add integration tests for IM command handling, especially workspace addition with various path states.
  • Simulate concurrent plan approval and RunCheckpointed events to verify state machine correctness.
  • Add integration tests for config file read/save permissions across platforms.
  • Add test for WeChat adapter token refresh and session expiry.
  • Add test for WeCom adapter heartbeat loss and reconnection.
  • Add fuzz tests for message parsing in both adapters to catch panics on malformed JSON.
  • Create a mock PlatformAdapter trait implementation for use in gateway_runner integration tests.
  • Use reqwest::Client mocks (wiremock or similar) for WeixinAdapter HTTP-based tests.
  • Use an in-memory SQLite database for UserSession and GatewayConfig persistence tests.
  • Add a local tokio-tungstenite WebSocket server harness for WecomAdapter integration tests.
  • Extend the existing message_formatter test suite with markdown normalization edge cases.
  • Add a test helper that creates GatewayState from an in-memory database for runner-level integration tests.

File-Level Coverage Notes

  • src-tauri/src/gateway/config.rs: added with no tests (Existing default-function correctness is covered by serde defaults but not tested explicitly)
  • src-tauri/src/gateway/gateway_runner.rs: added with no tests (Extensive inline documentation suggests testability intent but no tests were added)
  • src-tauri/src/gateway/mod.rs: low risk, thin orchestration layer (The run entry point delegates to gateway_runner::run; testing should focus on the runner)
  • src-tauri/src/gateway/traits.rs: low risk, trait definitions only (No logic to test; all implementation is in platform adapters)
  • src-tauri/src/gateway/user_session.rs: added with no tests (GatewaySessionRow query and upsert SQL are direct—syntax errors would be caught at runtime but ideally tested)
  • src-tauri/src/gateway/command_router.rs: partially tested—core command parsing is well covered (Existing tests cover the primary command variants; the TODO comment at line 3 is stale)
  • src-tauri/src/gateway/approval_bridge.rs: well tested for parsing, formatting, and truncation (Approval resolution function depends on ToolGateway and would need an integration test)
  • src-tauri/src/gateway/message_formatter.rs: partially tested—split logic is covered, normalization is not (Existing tests cover split_by_paragraph and basic short/long cases well)
  • src-tauri/src/gateway/platforms/mod.rs: low risk, module declarations only (No logic to test)
  • src-tauri/src/gateway/platforms/weixin.rs: partially tested—text splitting has unit tests; adapter lifecycle, dedup, and polling are untested (split_text tests are good; adapter-level tests require HTTP mocking infrastructure)
  • src-tauri/src/gateway/platforms/weixin_auth.rs: added with no tests (QR generation and session persistence are pure functions that are easy to test)
  • src-tauri/src/gateway/platforms/weixin_media.rs: well tested for encryption, decryption, URL building, and attachment extraction (The existing test suite covers the core cryptography and data-mapping logic thoroughly)
  • src-tauri/src/gateway/platforms/wecom.rs: added with no tests (The adapter is complete but lacks any automated validation of its WebSocket protocol handling)
  • src-tauri/src/core/gateway_supervisor.rs: No tests; critical coverage gap for process management.
  • src-tauri/src/commands/gateway.rs: All new commands lack tests; low risk but should be covered.
  • src-tauri/Cargo.toml: Dependency additions only; no direct test impact.
  • src-tauri/src/lib.rs: run_gateway and command registration are new; no tests for gateway bootstrap logic.
  • src-tauri/src/core/agent_session_compression.rs: Panic recovery logic is added but untested; high risk of silent failures.
  • src-tauri/src/core/index_manager.rs: New is_dir_blocked helper lacks tests for prefix matching.
  • src-tauri/src/core/plan_checkpoint.rs: New plan_design_markdown is well-tested; no issues.
  • ... and 14 more file-level entries.

Inline Downgraded Items (processed but not inline)

  • src-tauri/src/core/gateway_supervisor.rs: Missing tests for GatewaySupervisor lifecycle and process utilities (file_level_finding)
  • src-tauri/src/core/agent_session_compression.rs: No test for panic recovery in compression post-persist (file_level_finding)
  • src/modules/settings-center/ui/gateway-settings-panel.tsx: No unit tests for new GatewaySettingsPanel logic (file_level_finding)
  • src-tauri/src/commands/gateway.rs: No tests for gateway Tauri commands (file_level_finding)

Coverage Status

  • Target files: 34
  • Covered files: 34
  • Uncovered files: 0
  • No-patch/binary covered as file-level: 0
  • Findings with unknown confidence (N/A): 0

Uncovered list:

  • None

No-patch covered list:

  • None

Runtime/Budget

  • Rounds used: 2/4
  • Planned batches: 3
  • Executed batches: 3
  • Sub-agent runs: 11
  • Planner calls: 2
  • Reviewer calls: 12
  • Model calls: 14/64
  • Structured-output summary-only degradation: NO

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 17
  • Findings with unknown confidence: 0
  • Inline comments attempted: 10
  • Target files: 27
  • Covered files: 27
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

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.

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.


use tauri::State;

use crate::core::gateway_supervisor::{GatewayStatus, GatewaySupervisorHandle};

This comment was marked as outdated.

}

GatewayCommand::Stop => {
if let SessionState::AgentRunning { .. } = session.state {

This comment was marked as outdated.

&& 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.

let child = Command::new(&exe)
.args([
"gateway",
"--config",

This comment was marked as outdated.

let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
"Authorization",
format!("Bearer {}", *token).parse().unwrap(),

This comment was marked as outdated.

}

/// Build the standard iLink API headers.
async fn api_headers(&self) -> reqwest::header::HeaderMap {

This comment was marked as outdated.

}
};

const pollLogin = async (uuid: string) => {

This comment was marked as outdated.

}

/// Build the full API URL for an endpoint.
fn api_url(&self, endpoint: &str) -> String {

This comment was marked as outdated.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 13
  • Findings with unknown confidence: 0
  • Inline comments attempted: 12
  • Target files: 27
  • Covered files: 27
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

@@ -0,0 +1,429 @@
/**

This comment was marked as outdated.

/// 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.

Comment thread src-tauri/src/lib.rs
})
}

pub fn run_gateway(config_path: Option<&str>) -> anyhow::Result<()> {

This comment was marked as outdated.

}
};

const pollLogin = async (uuid: string) => {

This comment was marked as outdated.

Comment thread src-tauri/Cargo.toml
aes = "0.8"
cbc = { version = "0.1", features = ["alloc"] }
rand = "0.8"
md-5 = "0.10"

This comment was marked as outdated.

Comment thread src-tauri/src/lib.rs
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.

Comment thread src-tauri/src/lib.rs

// 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.

wecomWsUrl: string;
}>) => {
try {
await invoke("gateway_save_config", {

This comment was marked as outdated.

}
};

const pollLogin = async (uuid: string) => {

This comment was marked as outdated.

Comment thread src-tauri/Cargo.toml

[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.

jorben added 6 commits May 29, 2026 10:44
…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.
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 17
  • Findings with unknown confidence: 0
  • Inline comments attempted: 13
  • Target files: 27
  • Covered files: 27
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

// 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.

}
}

#[async_trait::async_trait]

This comment was marked as outdated.

/// 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.

/// 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.

wecomSecret: string;
wecomWsUrl: string;
}>) => {
try {

This comment was marked as outdated.

}

/// Parse a single update JSON into an InboundMessage (with dedup).
fn parse_update(

This comment was marked as outdated.

Comment thread src-tauri/src/lib.rs

app.manage(state);
app.manage(desktop_runtime);
app.manage(crate::core::gateway_supervisor::GatewaySupervisorHandle::new());

This comment was marked as outdated.

Comment thread src-tauri/src/lib.rs
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.

}
};

const pollLogin = async (uuid: string) => {

This comment was marked as outdated.

Comment thread src-tauri/src/lib.rs
commands::terminal::terminal_close,
commands::terminal::terminal_list,
commands::terminal::terminal_list_available_shells,
// Gateway

This comment was marked as outdated.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 13
  • Findings with unknown confidence: 0
  • Inline comments attempted: 13
  • Target files: 29
  • Covered files: 29
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

//! directly (bypassing ACP) via a command-routing + event-pump architecture.

pub mod approval_bridge;
pub mod command_router;

This comment was marked as outdated.

@@ -0,0 +1,312 @@
//! Gateway process supervisor — manages the lifecycle of the IM gateway child process.

This comment was marked as outdated.

.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.

// 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.

/// - 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.

wecomWsUrl: string;
}>) => {
try {
await invoke("gateway_save_config", {

This comment was marked as outdated.

@@ -0,0 +1,258 @@
//! Gateway configuration structures and TOML loading.

This comment was marked as outdated.

}
};

const pollLogin = async (uuid: string) => {

This comment was marked as outdated.

use std::time::{Duration, SystemTime};

use futures::StreamExt;
use tokio::sync::{broadcast, mpsc};

This comment was marked as outdated.


/// 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.

jorben added 3 commits May 29, 2026 15:29
- 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
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 15
  • Findings with unknown confidence: 0
  • Inline comments attempted: 13
  • Target files: 32
  • Covered files: 32
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

/// 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.

/// 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.

/// 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.


impl GatewayConfig {
/// Load configuration from a TOML file.
pub fn load(path: &Path) -> anyhow::Result<Self> {

This comment was marked as outdated.

}

/// Save a gateway config to the default TOML path atomically.
pub fn save_config(config: &GatewayConfig) -> anyhow::Result<()> {

This comment was marked as outdated.

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.

".fleet",
"DerivedData",
".build",
"cmake-build-",

This comment was marked as outdated.

* 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.

weixinEnabled,
wecomEnabled,
wecomBotId,
wecomSecret: "",

This comment was marked as outdated.

} 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.

jorben added 5 commits May 29, 2026 21:56
移除飞书和 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.
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 17
  • Findings with unknown confidence: 0
  • Inline comments attempted: 10
  • Target files: 34
  • Covered files: 34
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

@@ -0,0 +1,1274 @@
//! Gateway runner — main event loop that drives the IM platform adapter,

This comment was marked as outdated.

}
}

fn safely_build_and_replace_compressed_messages(

This comment was marked as outdated.

path: path.clone(),
name,
};
match state.workspace_manager.add(input).await {

This comment was marked as outdated.

@@ -0,0 +1,329 @@
//! Message formatting and chunking for IM platforms — placeholder for Step 4.

This comment was marked as outdated.

}

/// Persist session to disk atomically.
pub fn save_session(session: &WeixinSession) -> anyhow::Result<()> {

This comment was marked as outdated.

}
};

const pollLogin = async (uuid: string) => {

This comment was marked as outdated.

/// 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.

}

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.

.unwrap_or_default()
}

/// Check whether a directory name should be excluded or lazy-loaded.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

[From SubAgent: testing]

"send rejected: errcode={errcode} errmsg={errmsg}"
)))
}
}

This comment was marked as outdated.

@jorben jorben merged commit 24a08de into master May 29, 2026
2 checks passed
@jorben jorben deleted the feat/channel-gateway branch May 29, 2026 16:50
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR review completed.

  • Findings kept: 16
  • Findings with unknown confidence: 0
  • Inline comments attempted: 12
  • Target files: 34
  • Covered files: 34
  • Uncovered files: 0
    See the summary comment for detailed analysis and coverage details.

}
}

impl Drop for GatewaySupervisor {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

[From SubAgent: general]

}

/// Core runner logic extracted for testability.
async fn run_with_adapter(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

[From SubAgent: testing]

// 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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

[From SubAgent: general]

///
/// 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(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

[From SubAgent: testing]

/// 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(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

[From SubAgent: testing]

...overrides,
},
});
setConfigDirty(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

[From SubAgent: general]


/// Validate that the required platform config section is present and
/// that required credentials are non-empty for enabled channels.
fn validate(&self) -> anyhow::Result<()> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

[From SubAgent: testing]

/// 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 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

[From SubAgent: testing]

setConfigDirty(false);
}, []);

const autoSaveConfig = useCallback(async (overrides: Partial<{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

[From SubAgent: testing]

{weixinSession ? (
<SettingsRow
label={weixinSession.account_id || "—"}
description={`Token: ${(weixinSession.token ?? "").slice(0, 8)}…`}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

[From SubAgent: security]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant