use anyhow::Context;
use serde::{Deserialize, Serialize};
use std::io::{self, IsTerminal, Write};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
fn default_pi_path() -> String {
"pi".to_string()
}
fn default_brain_addr() -> String {
"127.0.0.1:15000".to_string()
}
fn default_service_url() -> String {
"ws://127.0.0.1:18080".to_string()
}
fn default_cwd() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Settings {
#[serde(default)]
pub username: String,
#[serde(default)]
pub password: String,
#[serde(default = "default_service_url")]
pub service_url: String,
#[serde(default = "default_cwd")]
pub cwd: PathBuf,
#[serde(default = "default_pi_path")]
pub pi_path: String,
#[serde(default = "default_brain_addr")]
pub brain_addr: String,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub data_dir: Option<PathBuf>,
}
impl Default for Settings {
fn default() -> Self {
Self {
username: String::new(),
password: String::new(),
service_url: default_service_url(),
cwd: default_cwd(),
pi_path: default_pi_path(),
brain_addr: default_brain_addr(),
data_dir: None,
}
}
}
#[derive(Debug, Clone)]
pub struct AccountConfig {
pub username: String,
pub password: String,
}
impl Settings {
pub fn load() -> anyhow::Result<Self> {
let path = settings_path()?;
if !path.exists() {
std::fs::create_dir_all(data_home()?)?;
Self::default().save(&path)?;
println!("已创建默认配置: {}", path.display());
}
let content = std::fs::read_to_string(&path)
.with_context(|| format!("读取配置失败: {}", path.display()))?;
let mut settings: Self = serde_json::from_str(&content)
.with_context(|| format!("配置格式错误: {}", path.display()))?;
settings.apply_env();
if settings.needs_onboarding() {
if !io::stdin().is_terminal() {
anyhow::bail!(
"配置尚未完成,请编辑 {},填写 serviceUrl、username 和 password",
path.display()
);
}
settings.onboard()?;
settings.save(&path)?;
println!("配置已保存,正在启动...");
}
settings.validate()?;
settings.cwd = settings
.cwd
.canonicalize()
.with_context(|| format!("工作目录不存在: {}", settings.cwd.display()))?;
if !settings.cwd.is_dir() {
anyhow::bail!("cwd 不是目录: {}", settings.cwd.display());
}
settings
.data_dir
.get_or_insert(data_home()?.join("sessions"));
Ok(settings)
}
fn apply_env(&mut self) {
if let Ok(value) = std::env::var("XAGENT_SERVICE_URL") {
self.service_url = value;
}
if let Ok(value) = std::env::var("XAGENT_USERNAME") {
self.username = value;
}
if let Ok(value) = std::env::var("XAGENT_PASSWORD") {
self.password = value;
}
if let Some(value) = std::env::var_os("XAGENT_CWD") {
self.cwd = PathBuf::from(value);
}
if let Ok(value) = std::env::var("XAGENT_PI_PATH") {
self.pi_path = value;
}
if let Ok(value) = std::env::var("XAGENT_BRAIN_ADDR") {
self.brain_addr = value;
}
if let Some(value) = std::env::var_os("XAGENT_DATA_DIR") {
self.data_dir = Some(PathBuf::from(value));
}
}
fn needs_onboarding(&self) -> bool {
self.username.trim().is_empty()
|| self.password.len() < 6
|| self.service_url.trim().is_empty()
}
fn onboard(&mut self) -> anyhow::Result<()> {
println!("首次使用 xagent-pi,请完成连接配置。");
self.service_url = prompt("服务端地址", &self.service_url)?;
self.username = prompt("用户名", &self.username)?;
if self.password.len() < 6 {
loop {
let password = rpassword::prompt_password("密码(至少 6 个字符): ")?;
if password.len() >= 6 {
self.password = password;
break;
}
println!("密码至少需要 6 个字符。");
}
}
let cwd = prompt("工作目录", &self.cwd.to_string_lossy())?;
self.cwd = PathBuf::from(cwd);
Ok(())
}
fn validate(&self) -> anyhow::Result<()> {
if self.username.trim().is_empty() {
anyhow::bail!("用户名不能为空");
}
if self.password.len() < 6 {
anyhow::bail!("密码至少需要 6 个字符");
}
if self.service_url.trim().is_empty() {
anyhow::bail!("服务端地址不能为空");
}
Ok(())
}
fn save(&self, path: &Path) -> anyhow::Result<()> {
std::fs::write(path, format!("{}\n", serde_json::to_string_pretty(self)?))?;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
Ok(())
}
pub fn data_dir(&self) -> &Path {
self.data_dir.as_deref().expect("dataDir initialized")
}
pub fn account(&self) -> AccountConfig {
AccountConfig {
username: self.username.clone(),
password: self.password.clone(),
}
}
}
fn prompt(label: &str, current: &str) -> anyhow::Result<String> {
loop {
if current.is_empty() {
print!("{label}: ");
} else {
print!("{label} [{current}]: ");
}
io::stdout().flush()?;
let mut value = String::new();
if io::stdin().read_line(&mut value)? == 0 {
anyhow::bail!("输入已取消");
}
let value = value.trim();
let selected = if value.is_empty() { current } else { value };
if !selected.is_empty() {
return Ok(selected.to_string());
}
println!("{label}不能为空。");
}
}
pub fn data_home() -> anyhow::Result<PathBuf> {
let home = std::env::var_os("HOME").context("HOME 环境变量未设置")?;
Ok(PathBuf::from(home).join(".xagent"))
}
pub fn settings_path() -> anyhow::Result<PathBuf> {
Ok(data_home()?.join("setting.json"))
}
pub fn pid_path() -> anyhow::Result<PathBuf> {
Ok(data_home()?.join("xagent-pi.pid"))
}
pub fn log_path() -> anyhow::Result<PathBuf> {
Ok(data_home()?.join("xagent-pi.log"))
}