beam-cli 0.10.1

CLI entrypoint for beam, a Rust runtime that connects Feishu/Lark threads to local AI coding CLIs
use super::discover_session_id;
use crate::*;
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};

pub(crate) fn daemon_log_path(paths: &BeamPaths) -> PathBuf {
    paths.daemon_log()
}

pub(crate) fn load_bots(paths: &BeamPaths) -> Result<Vec<BotConfig>> {
    match std::fs::read_to_string(paths.bots_json()) {
        Ok(raw) => Ok(serde_json::from_str(&raw)?),
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
        Err(err) => Err(err.into()),
    }
}

pub(crate) fn load_sessions_from_store(
    paths: &BeamPaths,
) -> Result<std::collections::HashMap<String, Session>> {
    match std::fs::read(paths.session_store_json()) {
        Ok(bytes) => Ok(serde_json::from_slice(&bytes)?),
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
            Ok(std::collections::HashMap::new())
        }
        Err(err) => Err(err.into()),
    }
}

#[derive(Debug, Clone, Deserialize)]
pub(crate) struct BotInfoEntry {
    #[serde(rename = "larkAppId")]
    pub(crate) lark_app_id: String,
    #[serde(rename = "botOpenId")]
    pub(crate) bot_open_id: Option<String>,
    #[serde(rename = "botName")]
    pub(crate) bot_name: Option<String>,
    #[serde(rename = "cliId")]
    pub(crate) cli_id: String,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub(crate) struct BotListEntry {
    pub(crate) name: String,
    #[serde(rename = "openId")]
    pub(crate) open_id: String,
    #[serde(rename = "isSelf")]
    pub(crate) is_self: bool,
    source: &'static str,
    #[serde(rename = "larkAppId")]
    pub(crate) lark_app_id: String,
    #[serde(rename = "workflowBot")]
    workflow_bot: String,
    capability: Option<String>,
    #[serde(rename = "hasTeamRole")]
    has_team_role: bool,
    pub(crate) mentionable: bool,
    #[serde(rename = "mentionSource")]
    pub(crate) mention_source: &'static str,
}

pub(crate) fn load_bot_info_entries(paths: &BeamPaths) -> Result<Vec<BotInfoEntry>> {
    let path = paths.root().join("bots-info.json");
    match std::fs::read_to_string(path) {
        Ok(raw) => Ok(serde_json::from_str(&raw)?),
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
        Err(err) => Err(err.into()),
    }
}

pub(crate) fn format_bot_info_entries_for_cli(
    entries: &[BotInfoEntry],
    current_lark_app_id: &str,
) -> Vec<BotListEntry> {
    entries
        .iter()
        .filter_map(|entry| {
            let open_id = entry.bot_open_id.as_ref()?.clone();
            let is_self = entry.lark_app_id == current_lark_app_id;
            Some(BotListEntry {
                name: entry
                    .bot_name
                    .clone()
                    .unwrap_or_else(|| entry.cli_id.clone()),
                open_id,
                is_self,
                source: "configured",
                lark_app_id: entry.lark_app_id.clone(),
                workflow_bot: entry.lark_app_id.clone(),
                capability: None,
                has_team_role: false,
                mentionable: is_self,
                mention_source: if is_self { "self" } else { "fallback" },
            })
        })
        .collect()
}

pub(crate) fn arg_value(args: &[String], name: &str) -> Option<String> {
    args.windows(2)
        .find_map(|pair| (pair[0] == name).then(|| pair[1].clone()))
}

pub(crate) fn cmd_bots(args: Vec<String>, paths: &BeamPaths) -> Result<()> {
    let (sub, rest) = match args.first() {
        Some(first) if !first.starts_with('-') => (first.as_str(), &args[1..]),
        _ => ("list", args.as_slice()),
    };
    if sub != "list" && sub != "ls" {
        bail!("用法: beam bots list [--session-id ID]");
    }

    let session_id = match arg_value(rest, "--session-id") {
        Some(value) => value,
        None => discover_session_id(paths).map_err(|_| {
            anyhow::anyhow!(
                "无法推断 session-id。请在 Lark 话题内的 CLI 会话中运行,或传 --session-id <id>。"
            )
        })?,
    };
    let sessions = load_sessions_from_store(paths)?;
    let session = sessions
        .get(&session_id)
        .with_context(|| format!("未找到 session {}", session_id))?;
    if session.lark_app_id.is_empty() {
        bail!("session {} 缺少 larkAppId", session_id);
    }

    let bots =
        format_bot_info_entries_for_cli(&load_bot_info_entries(paths)?, &session.lark_app_id);
    let out = serde_json::json!({
        "sessionId": session_id,
        "chatId": session.chat_id,
        "bots": bots,
        "total": bots.len(),
    });
    println!("{}", serde_json::to_string_pretty(&out)?);
    Ok(())
}