feat: 模块统一 ias- 前缀、独立版本与内网地址支持 (0.1.7)

- 所有 Rust crate 统一加 ias- 前缀:ai → ias-ai、common → ias-common、context → ias-context、mail → ias-mail、service → ias-service、wechat → ias-wechat
- 新增 ias-main 模块承载 CLI 入口和二进制打包
- 各模块开始独立维护版本号,ias-main 0.1.7 代表整体版本
- 新增 IA_WEB_INTERNAL_URL 环境变量,版本通知同时发送公网和内网链接
- 将已有升级日志翻译为中文
- ias_upgrade_logs 新增 modules JSONB 字段
- 新增 VERSION 文件和 CHANGELOG.md
- 新增 AGENTS.md 项目规则文件
This commit is contained in:
2026-07-06 17:57:48 +08:00
parent ed7f89d44f
commit 25dd0daa8b
91 changed files with 5546 additions and 777 deletions
+119
View File
@@ -0,0 +1,119 @@
use std::env;
use std::time::Duration;
use anyhow::anyhow;
use crate::model::MailAccount;
#[derive(Debug, Clone)]
pub struct MailPollerConfig {
pub mail_account_id: Option<i64>,
pub name: String,
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
pub mailbox: String,
pub source_key: String,
pub notify_account_id: String,
pub notify_user_id: String,
pub poll_interval: Duration,
pub fetch_limit: usize,
pub accept_invalid_certs: bool,
}
impl MailPollerConfig {
pub fn from_account(account: MailAccount) -> Self {
Self {
mail_account_id: Some(account.id),
name: account.name,
source_key: account.source_key,
host: account.imap_host,
port: account.imap_port.clamp(1, u16::MAX as i32) as u16,
username: account.imap_username,
password: account.imap_password,
mailbox: account.mailbox,
notify_account_id: account.notify_account_id,
notify_user_id: account.notify_user_id,
poll_interval: Duration::from_secs(account.poll_seconds.max(10) as u64),
fetch_limit: account.fetch_limit.clamp(1, 100) as usize,
accept_invalid_certs: account.accept_invalid_certs,
}
}
pub fn from_env() -> anyhow::Result<Option<Self>> {
if !env_bool("IA_MAIL_ENABLED", true) {
return Ok(None);
}
let host = optional_env("IA_MAIL_IMAP_HOST");
let username = optional_env("IA_MAIL_IMAP_USERNAME");
let password = optional_env("IA_MAIL_IMAP_PASSWORD");
let notify_account_id = optional_env("IA_MAIL_NOTIFY_ACCOUNT_ID");
let notify_user_id = optional_env("IA_MAIL_NOTIFY_USER_ID")
.or_else(|| optional_env("IA_MAIL_NOTIFY_FROM_USER_ID"));
let required_values = [
host.as_ref(),
username.as_ref(),
password.as_ref(),
notify_account_id.as_ref(),
notify_user_id.as_ref(),
];
if required_values.iter().all(|value| value.is_none()) {
return Ok(None);
}
if required_values.iter().any(|value| value.is_none()) {
return Err(anyhow!(
"邮件轮询配置不完整,需要 IA_MAIL_IMAP_HOST、IA_MAIL_IMAP_USERNAME、IA_MAIL_IMAP_PASSWORD、IA_MAIL_NOTIFY_ACCOUNT_ID、IA_MAIL_NOTIFY_USER_ID"
));
}
let host = host.unwrap();
let username = username.unwrap();
let port = optional_env("IA_MAIL_IMAP_PORT")
.and_then(|value| value.parse::<u16>().ok())
.unwrap_or(993);
let mailbox = optional_env("IA_MAIL_IMAP_MAILBOX").unwrap_or_else(|| "INBOX".to_string());
let poll_seconds = optional_env("IA_MAIL_POLL_SECONDS")
.and_then(|value| value.parse::<u64>().ok())
.unwrap_or(60)
.max(10);
let fetch_limit = optional_env("IA_MAIL_FETCH_LIMIT")
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(10)
.clamp(1, 100);
Ok(Some(Self {
mail_account_id: None,
name: username.clone(),
source_key: format!("imap:{host}:{username}"),
host,
port,
username,
password: password.unwrap(),
mailbox,
notify_account_id: notify_account_id.unwrap(),
notify_user_id: notify_user_id.unwrap(),
poll_interval: Duration::from_secs(poll_seconds),
fetch_limit,
accept_invalid_certs: env_bool("IA_MAIL_ACCEPT_INVALID_CERTS", false),
}))
}
}
fn optional_env(name: &str) -> Option<String> {
env::var(name)
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
fn env_bool(name: &str, default: bool) -> bool {
env::var(name)
.map(|value| {
let value = value.trim().to_ascii_lowercase();
!matches!(value.as_str(), "0" | "false" | "no" | "off")
})
.unwrap_or(default)
}