crabmate 0.4.0

Rust AI agent: OpenAI-compatible chat/completions, function calling, HTTP serve, ops CLI
Documentation
//! GitHub 在线模式 HTTP handler(只读:仓库上下文与当前分支 PR checks)。

use axum::Json;
use axum::extract::State;
use std::path::Path;

use crate::tools::web_api::{github_pr_current_checks, github_repo_context};
use crate::web::app_state::AppStateHttpCore;
use crate::web::http_types::github::{
    GithubPrCurrentChecksData, GithubPrCurrentChecksResponse, GithubRepoContextData,
    GithubRepoContextResponse,
};
use crate::workspace::path::validate_effective_workspace_base;

async fn github_workspace_dir(http: &AppStateHttpCore) -> Result<std::path::PathBuf, String> {
    let base_str = http.effective_workspace_path().await;
    if base_str.trim().is_empty() {
        return Err("工作区未设置".to_string());
    }
    let base = Path::new(&base_str);
    let base_canonical = base
        .canonicalize()
        .map_err(|e| format!("无法解析工作区路径:{e}"))?;
    let cfg = http.cfg.read().await;
    validate_effective_workspace_base(&cfg, &base_canonical).map_err(|e| e.user_message())?;
    Ok(base_canonical)
}

struct GithubGhContext {
    allowed_commands: Vec<String>,
    max_output_len: usize,
    work_dir: std::path::PathBuf,
}

async fn github_gh_context(http: &AppStateHttpCore) -> Result<GithubGhContext, String> {
    let work_dir = github_workspace_dir(http).await?;
    let cfg = http.cfg.read().await;
    Ok(GithubGhContext {
        allowed_commands: cfg.command_exec.allowed_commands.to_vec(),
        max_output_len: cfg.command_exec.command_max_output_len,
        work_dir,
    })
}

pub async fn github_repo_context_handler(
    State(http): State<AppStateHttpCore>,
) -> Json<GithubRepoContextResponse> {
    let ctx = match github_gh_context(&http).await {
        Ok(c) => c,
        Err(e) => {
            return Json(GithubRepoContextResponse {
                data: GithubRepoContextData::default(),
                error: Some(e),
            });
        }
    };
    let GithubGhContext {
        allowed_commands,
        max_output_len,
        work_dir,
    } = ctx;
    match tokio::task::spawn_blocking(move || {
        github_repo_context(max_output_len, &allowed_commands, &work_dir)
    })
    .await
    {
        Ok(Ok(data)) => Json(GithubRepoContextResponse { data, error: None }),
        Ok(Err(e)) => Json(GithubRepoContextResponse {
            data: GithubRepoContextData::default(),
            error: Some(e),
        }),
        Err(e) => Json(GithubRepoContextResponse {
            data: GithubRepoContextData::default(),
            error: Some(format!("GitHub 上下文查询失败:{e}")),
        }),
    }
}

pub async fn github_pr_current_checks_handler(
    State(http): State<AppStateHttpCore>,
) -> Json<GithubPrCurrentChecksResponse> {
    let ctx = match github_gh_context(&http).await {
        Ok(c) => c,
        Err(e) => {
            return Json(GithubPrCurrentChecksResponse {
                data: GithubPrCurrentChecksData::default(),
                error: Some(e),
            });
        }
    };
    let GithubGhContext {
        allowed_commands,
        max_output_len,
        work_dir,
    } = ctx;
    match tokio::task::spawn_blocking(move || {
        github_pr_current_checks(max_output_len, &allowed_commands, &work_dir)
    })
    .await
    {
        Ok(Ok(data)) => Json(GithubPrCurrentChecksResponse { data, error: None }),
        Ok(Err(e)) => Json(GithubPrCurrentChecksResponse {
            data: GithubPrCurrentChecksData::default(),
            error: Some(e),
        }),
        Err(e) => Json(GithubPrCurrentChecksResponse {
            data: GithubPrCurrentChecksData::default(),
            error: Some(format!("PR checks 查询失败:{e}")),
        }),
    }
}