feat(gateway): add reversible chat pii redaction

This commit is contained in:
Kayphoon
2026-05-13 18:25:13 +08:00
parent 3c2497f019
commit 2958041dc7
43 changed files with 7770 additions and 126 deletions
@@ -191,6 +191,7 @@ pub(super) async fn execute_provider_quota_plan(
let error = match err {
GatewayError::UpstreamUnavailable { message, .. }
| GatewayError::ControlUnavailable { message, .. }
| GatewayError::Client { message, .. }
| GatewayError::Internal(message) => message,
};
let proxy_node_id = plan
@@ -304,6 +304,7 @@ fn admin_provider_ops_gateway_error_message(error: GatewayError) -> String {
match error {
GatewayError::UpstreamUnavailable { message, .. }
| GatewayError::ControlUnavailable { message, .. }
| GatewayError::Client { message, .. }
| GatewayError::Internal(message) => message,
}
}
@@ -176,6 +176,7 @@ pub(crate) fn build_admin_provider_summary_value(
"claude_code_advanced": config.and_then(|cfg| cfg.get("claude_code_advanced")).cloned(),
"pool_advanced": config.and_then(|cfg| cfg.get("pool_advanced")).cloned(),
"failover_rules": config.and_then(|cfg| cfg.get("failover_rules")).cloned(),
"chat_pii_redaction": config.and_then(|cfg| cfg.get("chat_pii_redaction")).cloned(),
"total_endpoints": total_endpoints,
"active_endpoints": active_endpoints,
"total_keys": total_keys,
@@ -129,6 +129,28 @@ pub(crate) fn normalize_pool_advanced_config(
}
}
pub(crate) fn normalize_chat_pii_redaction_config(
value: Option<serde_json::Value>,
) -> Result<Option<serde_json::Value>, String> {
let Some(value) = value else {
return Ok(None);
};
match value {
serde_json::Value::Null => Ok(None),
serde_json::Value::Object(mut map) => {
if map.len() != 1 || !map.contains_key("enabled") {
return Err("chat_pii_redaction 仅支持 enabled 布尔配置".to_string());
}
let enabled = map
.remove("enabled")
.and_then(|value| value.as_bool())
.ok_or_else(|| "chat_pii_redaction.enabled 必须是布尔值".to_string())?;
Ok(Some(serde_json::json!({ "enabled": enabled })))
}
_ => Err("chat_pii_redaction 必须是 JSON 对象".to_string()),
}
}
pub(crate) fn validate_vertex_api_formats(
provider_type: &str,
auth_type: &str,
@@ -177,7 +199,8 @@ mod tests {
use super::{
normalize_allow_auth_channel_mismatch_formats, normalize_api_format_json_object_keys,
normalize_api_format_list, normalize_auth_type, normalize_auth_type_by_format,
normalize_pool_advanced_config, normalize_provider_type_input, validate_vertex_api_formats,
normalize_chat_pii_redaction_config, normalize_pool_advanced_config,
normalize_provider_type_input, validate_vertex_api_formats,
};
use serde_json::json;
@@ -201,6 +224,26 @@ mod tests {
);
}
#[test]
fn normalize_chat_pii_redaction_requires_enabled_boolean_only() {
assert_eq!(
normalize_chat_pii_redaction_config(Some(json!({ "enabled": true })))
.expect("chat pii redaction should normalize"),
Some(json!({ "enabled": true }))
);
assert_eq!(
normalize_chat_pii_redaction_config(Some(
json!({ "enabled": true, "entities": ["email"] })
))
.unwrap_err(),
"chat_pii_redaction 仅支持 enabled 布尔配置"
);
assert_eq!(
normalize_chat_pii_redaction_config(Some(json!({ "enabled": "yes" }))).unwrap_err(),
"chat_pii_redaction.enabled 必须是布尔值"
);
}
#[test]
fn normalize_auth_type_supports_bearer() {
assert_eq!(
@@ -2,6 +2,7 @@ use crate::handlers::admin::provider::shared::payloads::AdminProviderCreateReque
use crate::handlers::admin::provider::shared::support::{
normalize_provider_billing_type, parse_optional_rfc3339_unix_secs,
};
use crate::handlers::admin::provider::write::normalize::normalize_chat_pii_redaction_config;
use crate::handlers::admin::provider::write::normalize::normalize_pool_advanced_config;
use crate::handlers::admin::provider::write::normalize::normalize_provider_type_input;
use crate::handlers::admin::request::AdminAppState;
@@ -134,6 +135,12 @@ pub(crate) async fn build_admin_create_provider_record(
}
config_map.insert("claude_code_advanced".to_string(), value);
}
if config_map.contains_key("chat_pii_redaction") {
let value = normalize_chat_pii_redaction_config(config_map.remove("chat_pii_redaction"))?;
if let Some(value) = value {
config_map.insert("chat_pii_redaction".to_string(), value);
}
}
let config = (!config_map.is_empty()).then_some(serde_json::Value::Object(config_map));
let now_unix_secs = SystemTime::now()
@@ -2,6 +2,7 @@ use crate::handlers::admin::provider::shared::payloads::AdminProviderUpdatePatch
use crate::handlers::admin::provider::shared::support::{
normalize_provider_billing_type, parse_optional_rfc3339_unix_secs,
};
use crate::handlers::admin::provider::write::normalize::normalize_chat_pii_redaction_config;
use crate::handlers::admin::provider::write::normalize::normalize_pool_advanced_config;
use crate::handlers::admin::provider::write::normalize::normalize_provider_type_input;
use crate::handlers::admin::request::AdminAppState;
@@ -225,14 +226,29 @@ pub(crate) async fn build_admin_update_provider_record(
updated.enable_format_conversion = enable_format_conversion;
}
let config_seed = if fields.contains("config") {
normalize_json_object(payload.config, "config")?
} else {
updated.config.clone()
};
let mut config_map = config_seed
let mut config_map = updated
.config
.clone()
.and_then(|value| value.as_object().cloned())
.unwrap_or_default();
if fields.contains("config") {
if fields.is_null("config") {
config_map.clear();
} else {
let value = normalize_json_object(payload.config, "config")?
.ok_or_else(|| "config 必须是 JSON 对象".to_string())?;
let serde_json::Value::Object(patch_map) = value else {
return Err("config 必须是 JSON 对象".to_string());
};
for (key, value) in patch_map {
if value.is_null() {
config_map.remove(&key);
} else {
config_map.insert(key, value);
}
}
}
}
if fields.contains("claude_code_advanced") {
if fields.is_null("claude_code_advanced") {
@@ -270,6 +286,13 @@ pub(crate) async fn build_admin_update_provider_record(
}
}
if config_map.contains_key("chat_pii_redaction") {
let value = normalize_chat_pii_redaction_config(config_map.remove("chat_pii_redaction"))?;
if let Some(value) = value {
config_map.insert("chat_pii_redaction".to_string(), value);
}
}
updated.config = (!config_map.is_empty()).then_some(serde_json::Value::Object(config_map));
updated.updated_at_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -666,6 +666,7 @@ fn admin_provider_oauth_gateway_error_message(error: GatewayError) -> String {
match error {
GatewayError::UpstreamUnavailable { message, .. }
| GatewayError::ControlUnavailable { message, .. }
| GatewayError::Client { message, .. }
| GatewayError::Internal(message) => message,
}
}
@@ -55,6 +55,18 @@ pub(crate) const ADMIN_MODULE_DEFINITIONS: &[AdminModuleDefinition] = &[
admin_menu_group: None,
admin_menu_order: 0,
},
AdminModuleDefinition {
name: "chat_pii_redaction",
display_name: "敏感信息替换保护",
description: "发送给供应商前将聊天消息中的敏感信息替换为占位符,返回客户端前自动还原。",
category: "security",
env_key: "CHAT_PII_REDACTION_AVAILABLE",
default_available: true,
admin_route: Some("/admin/modules/chat-pii-redaction"),
admin_menu_icon: Some("ShieldCheck"),
admin_menu_group: Some("system"),
admin_menu_order: 59,
},
AdminModuleDefinition {
name: "notification_email",
display_name: "异常通知",