codex-mobile-bridge 0.3.14

Remote bridge and service manager for codex-mobile.
Documentation
use anyhow::Result;
use serde_json::{Value, json};

use crate::bridge_protocol::{
    AppServerHandshakeSummary, ThreadStatusInfo, ThreadTokenUsage, now_millis,
};
use crate::state::BridgeState;
use crate::state::helpers::{normalize_thread, optional_string};

pub(super) fn normalize_thread_summary(
    runtime_id: &str,
    thread_value: &Value,
    archived: bool,
) -> Option<crate::bridge_protocol::ThreadSummary> {
    normalize_thread(runtime_id, thread_value, archived).ok()
}

pub(super) fn thread_status_info(status_value: &Value) -> ThreadStatusInfo {
    let kind = status_value
        .get("type")
        .and_then(Value::as_str)
        .unwrap_or_else(|| status_value.as_str().unwrap_or("unknown"))
        .to_string();
    let reason = optional_string(status_value, "reason");
    ThreadStatusInfo {
        kind,
        reason,
        raw: status_value.clone(),
    }
}

pub(super) fn thread_token_usage(value: &Value) -> ThreadTokenUsage {
    ThreadTokenUsage {
        input_tokens: first_i64(value, &["inputTokens", "input_tokens"]),
        cached_input_tokens: first_i64(value, &["cachedInputTokens", "cached_input_tokens"]),
        output_tokens: first_i64(value, &["outputTokens", "output_tokens"]),
        reasoning_tokens: first_i64(value, &["reasoningTokens", "reasoning_tokens"]),
        total_tokens: first_i64(value, &["totalTokens", "total_tokens"]),
        raw: value.clone(),
        updated_at_ms: now_millis(),
    }
}

fn first_i64(value: &Value, keys: &[&str]) -> Option<i64> {
    keys.iter()
        .find_map(|key| value.get(*key).and_then(Value::as_i64))
}

pub(super) fn handshake_summary(
    state: &str,
    experimental_api_enabled: bool,
    opt_out_notification_methods: Vec<String>,
    detail: Option<String>,
) -> AppServerHandshakeSummary {
    AppServerHandshakeSummary::new(
        state.to_string(),
        experimental_api_enabled,
        opt_out_notification_methods,
        detail,
    )
}

pub(super) fn emit_unhandled_notification_notice(
    state: &BridgeState,
    runtime_id: &str,
    method: &str,
    params: &Value,
) -> Result<()> {
    let key = format!("app_server_unhandled_notification:{runtime_id}:{method}");
    if !state.should_emit_rate_limited_notice(&key) {
        return Ok(());
    }
    state.emit_event(
        "app_server_log_chunk",
        Some(runtime_id),
        params.get("threadId").and_then(Value::as_str),
        json!({
            "runtimeId": runtime_id,
            "stream": "protocol",
            "level": "warn",
            "source": "bridge.protocol",
            "message": format!("收到未处理通知: {method}"),
            "detail": {
                "kind": "unhandled_notification",
                "method": method,
            },
            "occurredAtMs": now_millis(),
        }),
    )
}