mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 08:27:46 +08:00
feat: restructure notification services
This commit is contained in:
@@ -33,6 +33,21 @@ fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value> {
|
||||
admin_system_config_default_value_pure(key)
|
||||
}
|
||||
|
||||
fn legacy_admin_system_config_fallback_key(normalized_key: &str) -> Option<&'static str> {
|
||||
match normalized_key {
|
||||
"module.server_chan_push.enabled" => {
|
||||
Some("module.important_notification.server_chan_enabled")
|
||||
}
|
||||
"module.server_chan_push.send_key" => {
|
||||
Some("module.important_notification.server_chan_send_key")
|
||||
}
|
||||
"module.server_chan_push.template" => {
|
||||
Some("module.important_notification.server_chan_template")
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_admin_system_configs_payload(
|
||||
entries: &[aether_data::repository::system::StoredSystemConfigEntry],
|
||||
) -> serde_json::Value {
|
||||
@@ -44,12 +59,14 @@ pub(crate) async fn build_admin_system_config_detail_payload(
|
||||
requested_key: &str,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
let requested_key = requested_key.trim();
|
||||
let value = state
|
||||
.read_system_config_json_value(&normalize_admin_system_config_key(requested_key))
|
||||
.await?
|
||||
.or_else(|| {
|
||||
admin_system_config_default_value(&normalize_admin_system_config_key(requested_key))
|
||||
});
|
||||
let normalized_key = normalize_admin_system_config_key(requested_key);
|
||||
let mut value = state.read_system_config_json_value(&normalized_key).await?;
|
||||
if value.is_none() {
|
||||
if let Some(legacy_key) = legacy_admin_system_config_fallback_key(&normalized_key) {
|
||||
value = state.read_system_config_json_value(legacy_key).await?;
|
||||
}
|
||||
}
|
||||
let value = value.or_else(|| admin_system_config_default_value(&normalized_key));
|
||||
Ok(build_admin_system_config_detail_payload_pure(
|
||||
requested_key,
|
||||
value,
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::important_notification::{
|
||||
important_notification_configured, IMPORTANT_NOTIFICATION_ENABLED_KEY,
|
||||
LEGACY_NOTIFICATION_EMAIL_ENABLED_KEY,
|
||||
};
|
||||
use crate::server_chan_push::server_chan_push_configured;
|
||||
use crate::system_features::ENABLE_MODEL_DIRECTIVES_CONFIG_KEY;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::system as admin_system_kernel;
|
||||
@@ -73,16 +74,28 @@ pub(crate) const ADMIN_MODULE_DEFINITIONS: &[AdminModuleDefinition] = &[
|
||||
},
|
||||
AdminModuleDefinition {
|
||||
name: "important_notification",
|
||||
display_name: "重要通知",
|
||||
description: "统一发送邮件和 Server 酱重要通知,供额度提醒等后台任务使用",
|
||||
display_name: "通知服务",
|
||||
description: "统一管理通知项、模板和推送服务选择,供后台任务和用户通知使用",
|
||||
category: "integration",
|
||||
env_key: "IMPORTANT_NOTIFICATION_AVAILABLE",
|
||||
default_available: true,
|
||||
admin_route: Some("/admin/modules/important-notification"),
|
||||
admin_route: Some("/admin/notification-service"),
|
||||
admin_menu_icon: Some("BellRing"),
|
||||
admin_menu_group: Some("system"),
|
||||
admin_menu_group: None,
|
||||
admin_menu_order: 58,
|
||||
},
|
||||
AdminModuleDefinition {
|
||||
name: "server_chan_push",
|
||||
display_name: "Server 酱推送",
|
||||
description: "第三方推送服务,配置 Server 酱 Turbo SendKey 并测试微信推送",
|
||||
category: "integration",
|
||||
env_key: "SERVER_CHAN_PUSH_AVAILABLE",
|
||||
default_available: true,
|
||||
admin_route: Some("/admin/modules/server-chan"),
|
||||
admin_menu_icon: Some("Send"),
|
||||
admin_menu_group: Some("system"),
|
||||
admin_menu_order: 59,
|
||||
},
|
||||
AdminModuleDefinition {
|
||||
name: "model_directives",
|
||||
display_name: "模型后缀参数",
|
||||
@@ -155,6 +168,7 @@ pub(crate) struct AdminModuleRuntimeState {
|
||||
ldap_config: Option<aether_data::repository::auth_modules::StoredLdapModuleConfig>,
|
||||
gemini_files_has_capable_key: bool,
|
||||
important_notification_configured: bool,
|
||||
server_chan_push_configured: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn admin_module_by_name(name: &str) -> Option<&'static AdminModuleDefinition> {
|
||||
@@ -242,12 +256,14 @@ pub(crate) async fn build_admin_module_runtime_state(
|
||||
};
|
||||
|
||||
let notification_configured = important_notification_configured(state.app()).await?;
|
||||
let server_chan_configured = server_chan_push_configured(state.app()).await?;
|
||||
|
||||
Ok(AdminModuleRuntimeState {
|
||||
oauth_providers,
|
||||
ldap_config,
|
||||
gemini_files_has_capable_key,
|
||||
important_notification_configured: notification_configured,
|
||||
server_chan_push_configured: server_chan_configured,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -261,6 +277,7 @@ pub(crate) fn build_admin_module_validation_result(
|
||||
runtime.ldap_config.as_ref(),
|
||||
runtime.gemini_files_has_capable_key,
|
||||
runtime.important_notification_configured,
|
||||
runtime.server_chan_push_configured,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ use axum::{
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::handlers::shared::{deserialize_optional_json_patch, normalize_feature_settings};
|
||||
use crate::handlers::shared::{
|
||||
deserialize_optional_json_patch, normalize_user_self_feature_settings_update,
|
||||
};
|
||||
|
||||
use super::{
|
||||
auth_password_policy_level, build_auth_error_response, resolve_authenticated_local_user,
|
||||
@@ -65,12 +67,24 @@ pub(super) async fn handle_users_me_detail_put(
|
||||
let email = normalize_users_me_optional_non_empty_string(payload.email);
|
||||
let username = normalize_users_me_optional_non_empty_string(payload.username);
|
||||
let feature_settings = match payload.feature_settings {
|
||||
Some(value) => match normalize_feature_settings(value) {
|
||||
Ok(value) => Some(value),
|
||||
Err(detail) => {
|
||||
return build_auth_error_response(http::StatusCode::BAD_REQUEST, detail, false);
|
||||
Some(value) => {
|
||||
let current = match state.read_user_feature_settings(&auth.user.id).await {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("user feature settings lookup failed: {err:?}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
match normalize_user_self_feature_settings_update(value, current) {
|
||||
Ok(value) => Some(value),
|
||||
Err(detail) => {
|
||||
return build_auth_error_response(http::StatusCode::BAD_REQUEST, detail, false);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ pub(crate) use self::external_models::OFFICIAL_EXTERNAL_MODEL_PROVIDERS;
|
||||
pub(crate) use self::normalize::{
|
||||
deserialize_optional_json_patch, deserialize_optional_string_list_patch, ip_rules_allow,
|
||||
json_ip_rules_allow, normalize_feature_settings, normalize_ip_rules, normalize_json_array,
|
||||
normalize_json_object, normalize_string_list, parse_json_ip_rules,
|
||||
normalize_json_object, normalize_string_list, normalize_user_self_feature_settings_update,
|
||||
parse_json_ip_rules,
|
||||
};
|
||||
pub(crate) use self::payloads::{
|
||||
InternalGatewayAuthContextRequest, InternalGatewayExecuteRequest,
|
||||
|
||||
@@ -54,6 +54,7 @@ pub(crate) fn normalize_feature_settings(value: Option<Value>) -> Result<Option<
|
||||
Value::Null => Ok(None),
|
||||
Value::Object(ref mut settings) => {
|
||||
normalize_chat_pii_redaction_feature_settings(settings)?;
|
||||
normalize_notification_push_service_feature_settings(settings)?;
|
||||
if settings.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
@@ -64,6 +65,41 @@ pub(crate) fn normalize_feature_settings(value: Option<Value>) -> Result<Option<
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_user_self_feature_settings_update(
|
||||
value: Option<Value>,
|
||||
current: Option<Value>,
|
||||
) -> Result<Option<Value>, String> {
|
||||
let mut normalized = normalize_feature_settings(value)?;
|
||||
let current_notification_push_service = current
|
||||
.and_then(|value| match value {
|
||||
Value::Object(mut settings) => settings.remove("notification_push_service"),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(|value| {
|
||||
let mut wrapper = Map::new();
|
||||
wrapper.insert("notification_push_service".to_string(), value);
|
||||
normalize_notification_push_service_feature_settings(&mut wrapper)
|
||||
.ok()
|
||||
.and_then(|_| wrapper.remove("notification_push_service"))
|
||||
});
|
||||
|
||||
match (&mut normalized, current_notification_push_service) {
|
||||
(Some(Value::Object(settings)), Some(value)) => {
|
||||
settings.insert("notification_push_service".to_string(), value);
|
||||
}
|
||||
(Some(Value::Object(settings)), None) => {
|
||||
settings.remove("notification_push_service");
|
||||
}
|
||||
(None, Some(value)) => {
|
||||
let mut settings = Map::new();
|
||||
settings.insert("notification_push_service".to_string(), value);
|
||||
normalized = Some(Value::Object(settings));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_ip_rules(
|
||||
values: Option<Vec<String>>,
|
||||
) -> Result<Option<Vec<String>>, String> {
|
||||
@@ -326,9 +362,47 @@ fn normalize_chat_pii_redaction_feature_object(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_notification_push_service_feature_settings(
|
||||
settings: &mut Map<String, Value>,
|
||||
) -> Result<(), String> {
|
||||
let Some(value) = settings.get_mut("notification_push_service") else {
|
||||
return Ok(());
|
||||
};
|
||||
match value {
|
||||
Value::Null => {
|
||||
settings.remove("notification_push_service");
|
||||
Ok(())
|
||||
}
|
||||
Value::Object(feature) => {
|
||||
normalize_notification_push_service_feature_object(feature)?;
|
||||
if feature.is_empty() {
|
||||
settings.remove("notification_push_service");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Err("notification_push_service 必须是对象".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_notification_push_service_feature_object(
|
||||
feature: &mut Map<String, Value>,
|
||||
) -> Result<(), String> {
|
||||
for key in ["enabled"] {
|
||||
if let Some(value) = feature.get(key) {
|
||||
if !value.is_boolean() {
|
||||
return Err(format!("notification_push_service.{key} 必须是布尔值"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ip_rules_allow, json_ip_rules_allow, normalize_ip_rules, parse_json_ip_rules};
|
||||
use super::{
|
||||
ip_rules_allow, json_ip_rules_allow, normalize_feature_settings, normalize_ip_rules,
|
||||
normalize_user_self_feature_settings_update, parse_json_ip_rules,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
@@ -358,6 +432,41 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_feature_settings_accepts_notification_push_service_permission() {
|
||||
let normalized = normalize_feature_settings(Some(json!({
|
||||
"notification_push_service": {"enabled": true}
|
||||
})))
|
||||
.expect("feature settings should normalize")
|
||||
.expect("feature settings should remain set");
|
||||
|
||||
assert_eq!(
|
||||
normalized["notification_push_service"]["enabled"],
|
||||
json!(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_self_feature_update_preserves_notification_push_permission() {
|
||||
let normalized = normalize_user_self_feature_settings_update(
|
||||
Some(json!({
|
||||
"chat_pii_redaction": {"enabled": true, "inject_model_instruction": false},
|
||||
"notification_push_service": {"enabled": false}
|
||||
})),
|
||||
Some(json!({
|
||||
"notification_push_service": {"enabled": true}
|
||||
})),
|
||||
)
|
||||
.expect("feature settings should normalize")
|
||||
.expect("feature settings should remain set");
|
||||
|
||||
assert_eq!(
|
||||
normalized["notification_push_service"]["enabled"],
|
||||
json!(true)
|
||||
);
|
||||
assert_eq!(normalized["chat_pii_redaction"]["enabled"], json!(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_rules_allow_applies_allow_rules_and_deny_overrides() {
|
||||
let rules = vec![
|
||||
|
||||
Reference in New Issue
Block a user