use super::load_bots;
use crate::*;
use anyhow::{Context, Result, bail};
use beam_core::cli_specs::{CLI_SPECS, cli_spec};
pub(crate) fn setup_backup_file(path: &Path) -> Result<Option<PathBuf>> {
if !path.exists() {
return Ok(None);
}
let backup = path.with_extension(format!(
"{}.bak",
path.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("bak")
));
std::fs::copy(path, &backup)?;
Ok(Some(backup))
}
pub(crate) async fn validate_setup_credentials(app_id: &str, app_secret: &str) -> Result<()> {
if std::env::var("BEAM_SKIP_SETUP_VALIDATION").ok().as_deref() == Some("1") {
println!("⚠️ 已跳过远程凭证校验(BEAM_SKIP_SETUP_VALIDATION=1)。");
return Ok(());
}
let client = reqwest::Client::new();
let resp = client
.post("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal")
.json(&serde_json::json!({
"app_id": app_id,
"app_secret": app_secret,
}))
.send()
.await
.context("failed to reach Feishu/Lark credential endpoint")?;
let status = resp.status();
let body: serde_json::Value = resp.json().await.unwrap_or_else(|_| serde_json::json!({}));
if !status.is_success() {
bail!("凭证校验失败: HTTP {}", status);
}
match body.get("code").and_then(|v| v.as_i64()).unwrap_or(-1) {
0 => Ok(()),
code => bail!(
"凭证校验失败: code={} msg={}",
code,
body.get("msg").and_then(|v| v.as_str()).unwrap_or("")
),
}
}
pub(crate) fn default_cli_args_for_cli_id(cli_id: &str) -> Vec<String> {
cli_spec(cli_id)
.map(|spec| spec.default_cli_args.iter().map(ToString::to_string).collect())
.unwrap_or_default()
}
pub(crate) fn parse_cli_args_input(input: &str, defaults: &[String]) -> Vec<String> {
let trimmed = input.trim();
if trimmed.is_empty() {
return defaults.to_vec();
}
if matches!(trimmed.to_ascii_lowercase().as_str(), "clear" | "none") {
return Vec::new();
}
trimmed
.split_whitespace()
.map(ToString::to_string)
.collect()
}
pub(crate) fn detect_installed_clis() -> Vec<&'static beam_core::cli_specs::CliSpec> {
CLI_SPECS
.iter()
.filter(|spec| spec.bin_candidates.iter().any(|b| which_exists(b)))
.collect()
}
pub(crate) fn bin_candidates_for_cli_id(cli_id: &str) -> Option<&'static [&'static str]> {
cli_spec(cli_id).map(|spec| spec.bin_candidates)
}
pub(crate) fn probe_cli_bin(cli_id: &str) -> Option<String> {
bin_candidates_for_cli_id(cli_id)
.and_then(|bins| bins.iter().find(|b| which_exists(b)).copied())
.map(String::from)
}
pub(crate) fn resolve_allowed_users(input: &str, user_open_id: Option<&str>) -> Vec<String> {
let mut allowed: Vec<String> = input
.split(',')
.map(|item| item.trim().to_string())
.filter(|item| !item.is_empty())
.collect();
if let Some(open_id) = user_open_id {
if !allowed.iter().any(|item| item == open_id) {
allowed.push(open_id.to_string());
}
}
allowed
}
pub(crate) fn which_exists(bin: &str) -> bool {
std::process::Command::new("which")
.arg(bin)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub(crate) fn prompt_cli_id() -> Result<String> {
let installed = detect_installed_clis();
if installed.is_empty() {
println!("未检测到已安装的 CLI 工具。");
let value = ask_line("请手动输入 CLI ID [claude-code]: ")?;
let value = value.trim();
if value.is_empty() {
return Ok("claude-code".to_string());
}
let valid_ids: Vec<&str> = CLI_SPECS.iter().map(|spec| spec.cli_id).collect();
if valid_ids.contains(&value) {
return Ok(value.to_string());
}
println!(
"不支持的 CLI ID \"{}\",支持: {}",
value,
valid_ids.join(", ")
);
return Ok("claude-code".to_string());
}
println!("已检测到以下 CLI 工具:");
for (i, spec) in installed.iter().enumerate() {
let bin_str = spec.bin_candidates.join(" / ");
println!(" {}) {} ({})", i + 1, spec.label, bin_str);
}
let value = ask_line("CLI 适配器 [1]: ")?;
let value = value.trim();
if value.is_empty() {
return Ok(installed[0].cli_id.to_string());
}
if let Ok(num) = value.parse::<usize>() {
if num >= 1 && num <= installed.len() {
return Ok(installed[num - 1].cli_id.to_string());
}
println!("无效序号 \"{}\",请输入 1-{}", num, installed.len());
} else {
let valid_ids: Vec<&str> = CLI_SPECS.iter().map(|spec| spec.cli_id).collect();
if valid_ids.contains(&value) {
return Ok(value.to_string());
}
println!(
"不支持的 CLI ID \"{}\",支持: {}",
value,
valid_ids.join(", ")
);
}
Ok(installed[0].cli_id.to_string())
}
pub(crate) async fn prompt_setup_bot() -> Result<BotConfig> {
let name = ask_line("机器人名称(留空=不设): ")?;
let credentials = register_app::prompt_credentials().await?;
let cli_id = prompt_cli_id()?;
let default_cli_args = default_cli_args_for_cli_id(&cli_id);
let default_cli_args_display = if default_cli_args.is_empty() {
"(无)".to_string()
} else {
default_cli_args.join(" ")
};
let cli_args_input = ask_line(&format!(
"启动参数 cliArgs [{}](回车使用默认,输入 clear 清空): ",
default_cli_args_display
))?;
let cli_args = parse_cli_args_input(&cli_args_input, &default_cli_args);
let cli_bin = probe_cli_bin(&cli_id).filter(|bin| bin != &cli_id);
let working_dir = {
let value = ask_line("默认工作目录 [~]: ")?;
if value.trim().is_empty() {
Some("~".to_string())
} else {
Some(value)
}
};
let skip_working_dir_prompt = {
let value = ask_line("是否跳过工作目录选择?[y/N]: ")?;
matches!(value.trim().to_lowercase().as_str(), "y" | "yes")
};
let allowed_users = {
let hint = if credentials.user_open_id.is_some() {
"允许用户 open_id(逗号分隔,留空=仅限自己,你的 open_id 已自动加入): "
} else {
"允许用户 open_id(逗号分隔,例如 ou_xxxxx;留空=不限制,任何人都可对话⚠️): "
};
let value = ask_line(hint)?;
let resolved = resolve_allowed_users(&value, credentials.user_open_id.as_deref());
if credentials.user_open_id.is_none() && resolved.is_empty() {
println!(" ⚠️ 未设置允许用户:当前为开放模式,任何人都可以和机器人对话。");
println!(
" 💡 可在 bots.json 中手动填写 allowedUsers 字段(open_id 以 ou_ 开头),或后续用 /grant 命令授权。"
);
}
resolved
};
Ok(BotConfig {
name: if name.trim().is_empty() {
None
} else {
Some(name)
},
lark_app_id: credentials.app_id,
lark_app_secret: credentials.app_secret,
cli_id,
cli_bin,
cli_args,
model: None,
working_dir,
skip_working_dir_prompt,
lark_encrypt_key: None,
lark_verification_token: None,
allowed_users,
private_card: false,
allowed_chat_groups: Vec::new(),
chat_grants: std::collections::HashMap::new(),
global_grants: Vec::new(),
oncall_chats: Vec::new(),
restrict_grant_commands: false,
message_quota: None,
quota_state: std::collections::HashMap::new(),
})
}
pub(crate) async fn cmd_setup(paths: &BeamPaths) -> Result<()> {
let root = paths.root();
std::fs::create_dir_all(root)?;
for dir in [
paths.logs_dir(),
paths.run_dir(),
paths.sessions_dir(),
paths.workflows_dir(),
paths.workflow_runs_dir(),
paths.state_dir(),
paths.cli_pid_markers_dir(),
paths.observed_bots_dir(),
paths.schedules_output_dir(),
] {
std::fs::create_dir_all(&dir)?;
}
let cfg = paths.config_toml();
if !cfg.exists() {
let defaults = "\
[daemon]\nworking_dirs = [\"~\"]\n\n\
[web]\nhost = \"0.0.0.0\"\nproxy_base_port = 8800\n\n\
";
std::fs::write(&cfg, defaults)?;
println!("Wrote {}", cfg.display());
} else {
println!("Config exists: {}", cfg.display());
}
let bots = paths.bots_json();
if !bots.exists() {
std::fs::write(&bots, "[]\n")?;
println!("Wrote {}", bots.display());
} else {
println!("Bots config exists: {}", bots.display());
}
println!("Setup complete. Data root: {}", root.display());
println!("现有 bots 数量: {}", load_bots(paths)?.len());
println!();
let existing = load_bots(paths)?;
let action = if existing.is_empty() {
"replace".to_string()
} else {
println!("已检测到现有机器人配置:");
for (i, bot) in existing.iter().enumerate() {
println!(
" {}. {} ({})",
i + 1,
bot.name.clone().unwrap_or_else(|| bot.cli_id.clone()),
bot.lark_app_id
);
}
ask_line("操作 [replace/add/skip] [replace]: ")?
};
let action = action.trim().to_lowercase();
let action = if action.is_empty() {
"replace".to_string()
} else {
action
};
if action == "skip" {
println!("已跳过 setup 写盘。");
return Ok(());
}
let mut next_bots = if action == "add" {
existing.clone()
} else {
Vec::new()
};
let next_bot = prompt_setup_bot().await?;
validate_setup_credentials(&next_bot.lark_app_id, &next_bot.lark_app_secret).await?;
next_bots.push(next_bot);
if bots.exists() {
if let Some(backup) = setup_backup_file(&bots)? {
println!("旧配置已备份: {}", backup.display());
}
}
std::fs::write(&bots, serde_json::to_string_pretty(&next_bots)? + "\n")?;
println!("✅ 已写入 {}", bots.display());
if let Err(err) = hook_setup::install_hooks() {
eprintln!("hook install skipped: {}", err);
} else {
println!("Installed hook config for Claude/OpenCode.");
}
println!("提示:先运行 `beam start`,再用 `beam autostart enable` 注册自启。");
Ok(())
}