codex-mobile-bridge 0.3.10

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

use crate::bridge_protocol::{
    PendingServerRequestOption, PendingServerRequestQuestion, PendingServerRequestRecord,
    now_millis,
};
use crate::state::BridgeState;
use crate::state::helpers::{optional_string, request_key};

pub(super) async fn handle_server_request(
    state: &BridgeState,
    runtime_id: &str,
    id: Value,
    method: &str,
    params: Value,
) -> Result<()> {
    match method {
        "item/commandExecution/requestApproval"
        | "item/fileChange/requestApproval"
        | "item/tool/requestUserInput"
        | "mcpServer/elicitation/request"
        | "item/permissions/requestApproval"
        | "item/tool/call"
        | "applyPatchApproval"
        | "execCommandApproval"
        | "account/chatgptAuthTokens/refresh" => {
            let request = pending_server_request(runtime_id, id, method, params)?;
            state.storage.put_pending_request(&request)?;
            state.emit_event(
                method,
                Some(runtime_id),
                request.thread_id.as_deref(),
                json!({ "request": request }),
            )?;
        }
        _ => {
            let runtime = state.require_runtime(Some(runtime_id)).await?;
            runtime
                .app_server
                .respond_error(id, -32000, "bridge 不支持的 server request")
                .await?;
        }
    }
    Ok(())
}

fn pending_server_request(
    runtime_id: &str,
    rpc_request_id: Value,
    request_type: &str,
    params: Value,
) -> Result<PendingServerRequestRecord> {
    let available_decisions = params
        .get("availableDecisions")
        .and_then(Value::as_array)
        .map(|items| {
            items
                .iter()
                .filter_map(Value::as_str)
                .map(ToOwned::to_owned)
                .collect::<Vec<_>>()
        })
        .unwrap_or_else(|| default_available_decisions(request_type));

    Ok(PendingServerRequestRecord {
        request_id: request_key(&rpc_request_id),
        runtime_id: runtime_id.to_string(),
        rpc_request_id,
        request_type: request_type.to_string(),
        request_kind: pending_request_kind(request_type).to_string(),
        thread_id: optional_string(&params, "threadId")
            .or_else(|| optional_string(&params, "conversationId")),
        turn_id: optional_string(&params, "turnId"),
        item_id: optional_string(&params, "itemId"),
        call_id: optional_string(&params, "callId"),
        title: pending_request_title(request_type),
        reason: optional_string(&params, "reason")
            .or_else(|| optional_string(&params, "message"))
            .or_else(|| optional_string(&params, "title")),
        command: pending_request_command(&params),
        cwd: optional_string(&params, "cwd"),
        grant_root: optional_string(&params, "grantRoot"),
        tool_name: optional_string(&params, "tool")
            .or_else(|| optional_string(&params, "name"))
            .or_else(|| optional_string(&params, "serverName")),
        arguments: params.get("arguments").cloned(),
        questions: pending_request_questions(&params),
        proposed_execpolicy_amendment: params.get("proposedExecpolicyAmendment").cloned(),
        network_approval_context: params.get("networkApprovalContext").cloned(),
        permissions: params.get("permissions").cloned(),
        schema: params
            .get("schema")
            .cloned()
            .or_else(|| params.get("requestedSchema").cloned())
            .or_else(|| params.get("inputSchema").cloned()),
        available_decisions,
        raw_payload: params,
        created_at_ms: now_millis(),
    })
}

fn pending_request_title(request_type: &str) -> Option<String> {
    Some(match request_type {
        "item/commandExecution/requestApproval" => "命令执行确认".to_string(),
        "item/fileChange/requestApproval" => "文件改动确认".to_string(),
        "item/tool/requestUserInput" => "需要补充输入".to_string(),
        "item/permissions/requestApproval" => "权限确认".to_string(),
        "item/tool/call" => "动态工具调用".to_string(),
        "mcpServer/elicitation/request" => "MCP 交互请求".to_string(),
        "applyPatchApproval" => "补丁应用确认".to_string(),
        "execCommandApproval" => "命令执行确认".to_string(),
        "account/chatgptAuthTokens/refresh" => "刷新 ChatGPT 认证".to_string(),
        _ => return None,
    })
}

fn pending_request_kind(request_type: &str) -> &'static str {
    match request_type {
        "item/commandExecution/requestApproval" => "commandExecutionApproval",
        "item/fileChange/requestApproval" => "fileChangeApproval",
        "item/tool/requestUserInput" => "toolRequestUserInput",
        "mcpServer/elicitation/request" => "mcpElicitationRequest",
        "item/permissions/requestApproval" => "permissionsApproval",
        "item/tool/call" => "dynamicToolCall",
        "applyPatchApproval" => "applyPatchApproval",
        "execCommandApproval" => "execCommandApproval",
        "account/chatgptAuthTokens/refresh" => "chatgptAuthTokensRefresh",
        _ => "unknown",
    }
}

fn default_available_decisions(request_type: &str) -> Vec<String> {
    match request_type {
        "item/commandExecution/requestApproval"
        | "item/fileChange/requestApproval"
        | "item/permissions/requestApproval" => vec![
            "accept".to_string(),
            "acceptForSession".to_string(),
            "decline".to_string(),
            "cancel".to_string(),
        ],
        "applyPatchApproval" | "execCommandApproval" => vec![
            "approved".to_string(),
            "approved_for_session".to_string(),
            "denied".to_string(),
            "abort".to_string(),
        ],
        _ => Vec::new(),
    }
}

fn pending_request_command(params: &Value) -> Option<String> {
    optional_string(params, "command").or_else(|| {
        params
            .get("command")
            .and_then(Value::as_array)
            .map(|parts| {
                parts
                    .iter()
                    .filter_map(Value::as_str)
                    .collect::<Vec<_>>()
                    .join(" ")
            })
            .filter(|command| !command.trim().is_empty())
    })
}

fn pending_request_questions(params: &Value) -> Vec<PendingServerRequestQuestion> {
    params
        .get("questions")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
        .map(|question| PendingServerRequestQuestion {
            id: optional_string(question, "id").unwrap_or_default(),
            header: optional_string(question, "header"),
            question: optional_string(question, "question")
                .or_else(|| optional_string(question, "prompt"))
                .or_else(|| optional_string(question, "label")),
            required: question
                .get("required")
                .and_then(Value::as_bool)
                .unwrap_or(false),
            options: question
                .get("options")
                .and_then(Value::as_array)
                .into_iter()
                .flatten()
                .map(|option| PendingServerRequestOption {
                    label: optional_string(option, "label")
                        .or_else(|| optional_string(option, "value"))
                        .unwrap_or_default(),
                    description: optional_string(option, "description"),
                    value: option.get("value").cloned(),
                    is_other: option
                        .get("isOther")
                        .and_then(Value::as_bool)
                        .unwrap_or(false),
                    raw: option.clone(),
                })
                .collect(),
            raw: question.clone(),
        })
        .collect()
}