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
+3 -2
View File
@@ -1,9 +1,10 @@
[package]
name = "ias-http"
version = "0.1.15"
version = "0.1.17"
edition = "2024"
[dependencies]
ias-common = { path = "../ias-common" }
anyhow = "1"
axum = "0.8.9"
chrono = { version = "0.4", features = ["serde"] }
@@ -15,6 +16,6 @@ sqlx = { version = "0.9.0", features = [
"postgres",
"chrono",
] }
tokio = { version = "1.0", features = ["net", "sync"] }
tokio = { version = "1.0", features = ["net", "sync", "io-util"] }
tower-http = { version = "0.6", features = ["cors", "fs"] }
uuid = { version = "1", features = ["v4", "serde"] }
+20 -5
View File
@@ -166,8 +166,19 @@ fn api_groups() -> Vec<ApiGroupDoc> {
"邮件详情",
"已读状态",
"邮箱目录过滤",
"可信发件人渲染",
],
keywords: &[
"mail",
"email",
"邮件",
"邮箱",
"未读",
"已读",
"搜索邮件",
"可信邮箱",
"邮件渲染",
],
keywords: &["mail", "email", "邮件", "邮箱", "未读", "已读", "搜索邮件"],
},
ApiGroupDoc {
id: "console",
@@ -420,13 +431,17 @@ fn api_docs() -> Vec<ApiDoc> {
method: "GET",
path: "/v1/mail/messages/{id}",
summary: "读取单封邮件记录",
description: "返回单封邮件详情;如果邮件尚未标记已读,读取时会尝试标记为已读。",
description: "返回单封邮件详情和渲染策略;如果邮件尚未标记已读,读取时会尝试标记为已读。",
auth: "",
query_params: &[],
path_params: &["id: 邮件记录 id"],
body: None,
response: r#"{"success":true,"message":{"id":1,"from_label":"...","subject":"...","content":"...","seen":true}}"#,
notes: &["邮件不存在时返回 404。"],
response: r#"{"success":true,"message":{"id":1,"from_label":"...","subject":"...","content":"...","seen":true},"rendering":{"trusted_sender":true,"remote_resources_allowed":true}}"#,
notes: &[
"rendering.trusted_sender 根据邮件账号可信发件人列表和 From 字段计算。",
"remote_resources_allowed=false 时前端会阻止 HTML 邮件中的远程图片资源。",
"邮件不存在时返回 404。",
],
},
ApiDoc {
id: "console.tools.list",
@@ -451,7 +466,7 @@ fn api_docs() -> Vec<ApiDoc> {
method: "GET",
path: "/v1/user-configs",
summary: "查询用户侧配置概览",
description: "返回微信账号和邮件账号配置的非敏感概览,密码或 token 只返回是否已配置。",
description: "返回微信账号和邮件账号配置的非敏感概览,邮件账号会包含推送标题关键字和可信发件人列表,密码或 token 只返回是否已配置。",
auth: "",
query_params: &[],
path_params: &[],
+37
View File
@@ -1,7 +1,10 @@
use axum::http::StatusCode;
use axum::routing::{any, get};
use axum::{Json, Router};
use ias_common::control::{ControlResponse, RuntimePaths};
use sqlx::PgPool;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixStream;
use tokio::sync::watch;
use tower_http::cors::{Any, CorsLayer};
use tower_http::services::{ServeDir, ServeFile};
@@ -26,6 +29,7 @@ pub async fn create_http(
let app = Router::new()
.route("/v1/health", get(health))
.route("/v1/daemon/status", get(daemon_status))
.merge(api_doc_routes::<()>())
.merge(log_routes::<()>(db.clone()))
.merge(update_routes::<()>(db.clone()))
@@ -66,6 +70,39 @@ async fn health() -> Json<serde_json::Value> {
}))
}
async fn daemon_status() -> Json<serde_json::Value> {
let paths = RuntimePaths::new();
match request_daemon_status(&paths).await {
Ok(snapshot) => Json(serde_json::json!({
"success": true,
"status": "ok",
"daemon": snapshot,
})),
Err(error) => Json(serde_json::json!({
"success": true,
"status": "degraded",
"message": "daemon control socket unavailable",
"error": error.to_string(),
})),
}
}
async fn request_daemon_status(
paths: &RuntimePaths,
) -> anyhow::Result<ias_common::control::RuntimeSnapshot> {
let mut stream = UnixStream::connect(&paths.socket_file).await?;
stream.write_all(b"status\n").await?;
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line).await?;
match serde_json::from_str(line.trim())? {
ControlResponse::Status { snapshot } => Ok(snapshot),
ControlResponse::Ack { message } | ControlResponse::Error { message } => {
anyhow::bail!(message)
}
}
}
async fn api_not_found() -> (StatusCode, Json<serde_json::Value>) {
(
StatusCode::NOT_FOUND,