feat: 拆分 worker 架构并增强 assistant 安全保护

- 新增 daemon/channel/assistant/function/scheduler/logging worker 架构和 JetStream 消息协议

- 修复邮件通知误入 assistant、迁移 checksum、日志 subject 和工具调用无限循环风险

- 新增 assistant 工具轮次熔断、工具结果截断和提示词软收敛规则

- 补充邮件推送关键字、可信发件人配置、升级日志和验证测试
This commit is contained in:
2026-07-10 00:41:36 +08:00
parent 7deae286ca
commit af6cfdaa83
70 changed files with 5953 additions and 1608 deletions
+99
View File
@@ -2,6 +2,7 @@ use axum::extract::{Path, Query};
use axum::http::StatusCode;
use axum::routing::get;
use axum::{Extension, Json, Router};
use serde::Serialize;
use serde_json::{Value, json};
use sqlx::PgPool;
@@ -17,6 +18,12 @@ pub struct MailRouteState {
pub db: PgPool,
}
#[derive(Debug, Clone, Serialize)]
struct MailRenderingSettings {
trusted_sender: bool,
remote_resources_allowed: bool,
}
impl MailRouteState {
pub fn new(db: PgPool) -> Self {
Self { db }
@@ -69,10 +76,12 @@ async fn get_message(
Err(error) => eprintln!("同步邮件已读失败 id={id}: {error:#}"),
}
}
let rendering = rendering_settings_for_message(&state.db, &message).await;
Ok(Json(json!({
"success": true,
"message": message,
"rendering": rendering,
})))
}
@@ -107,6 +116,67 @@ async fn mail_config_for_message(
Ok(None)
}
async fn rendering_settings_for_message(
pool: &PgPool,
message: &MailMessage,
) -> MailRenderingSettings {
let config = match mail_config_for_message(pool, message).await {
Ok(config) => config,
Err(error) => {
eprintln!("读取邮件渲染配置失败 id={}: {error:#}", message.id);
None
}
};
let trusted_sender = config
.as_ref()
.map(|config| sender_matches_trusted_list(&message.from_label, &config.trusted_senders))
.unwrap_or(false);
MailRenderingSettings {
trusted_sender,
remote_resources_allowed: trusted_sender,
}
}
fn sender_matches_trusted_list(from_label: &str, trusted_senders: &[String]) -> bool {
if trusted_senders.is_empty() {
return false;
}
let candidates = sender_candidates(from_label);
trusted_senders.iter().any(|trusted| {
let trusted_candidates = sender_candidates(trusted);
!trusted_candidates.is_empty()
&& trusted_candidates
.iter()
.any(|trusted| candidates.iter().any(|candidate| candidate == trusted))
})
}
fn sender_candidates(from_label: &str) -> Vec<String> {
let mut values = Vec::new();
if let Some(start) = from_label.find('<') {
if let Some(end) = from_label[start + 1..].find('>') {
push_sender_candidate(&mut values, &from_label[start + 1..start + 1 + end]);
}
} else {
// 没有尖括号时,整个 label 可能就是纯邮箱地址
push_sender_candidate(&mut values, from_label);
}
values
}
fn push_sender_candidate(values: &mut Vec<String>, value: &str) {
let value = value
.trim()
.trim_matches(['<', '>', '"', '\'', '(', ')', '[', ']'])
.to_ascii_lowercase();
if value.contains('@') && !values.iter().any(|existing| existing == &value) {
values.push(value);
}
}
fn api_not_found(message: impl Into<String>) -> ApiError {
(
StatusCode::NOT_FOUND,
@@ -127,3 +197,32 @@ fn api_internal_error(error: anyhow::Error) -> ApiError {
})),
)
}
#[cfg(test)]
mod tests {
use super::sender_matches_trusted_list;
#[test]
fn matches_trusted_sender_email_from_display_label() {
assert!(sender_matches_trusted_list(
"Facebook <reminders@facebookmail.com>",
&["reminders@facebookmail.com".to_string()]
));
}
#[test]
fn trusted_sender_matching_is_case_insensitive_and_exact() {
assert!(sender_matches_trusted_list(
"Alice <ALICE@example.com>",
&["alice@example.com".to_string()]
));
assert!(sender_matches_trusted_list(
"Alice <alice@example.com>",
&["Trusted <alice@example.com>".to_string()]
));
assert!(!sender_matches_trusted_list(
"mallory@example.net",
&["alice@example.com".to_string()]
));
}
}