mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-12 14:10:19 +08:00
Merge origin/main into fix/gemini-cli-v1internal
This commit is contained in:
@@ -385,7 +385,7 @@ fn resolve_admin_monitoring_usage_candidate_id(
|
||||
trace: &DecisionTrace,
|
||||
usage: &StoredRequestUsageAudit,
|
||||
) -> Option<String> {
|
||||
if usage.request_id.trim() != trace.request_id {
|
||||
if !admin_monitoring_usage_matches_trace(usage, trace.request_id.as_str()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -413,6 +413,43 @@ fn resolve_admin_monitoring_usage_candidate_id(
|
||||
.map(|item| item.candidate.id.clone())
|
||||
}
|
||||
|
||||
fn admin_monitoring_usage_matches_trace(
|
||||
usage: &StoredRequestUsageAudit,
|
||||
trace_request_id: &str,
|
||||
) -> bool {
|
||||
let trace_request_id = trace_request_id.trim();
|
||||
if trace_request_id.is_empty() {
|
||||
return false;
|
||||
}
|
||||
usage.request_id.trim() == trace_request_id
|
||||
|| usage
|
||||
.trace_id()
|
||||
.is_some_and(|value| value.trim() == trace_request_id)
|
||||
|| admin_monitoring_headers_contain_trace_id(
|
||||
usage.request_headers.as_ref(),
|
||||
trace_request_id,
|
||||
)
|
||||
|| admin_monitoring_headers_contain_trace_id(
|
||||
usage.provider_request_headers.as_ref(),
|
||||
trace_request_id,
|
||||
)
|
||||
}
|
||||
|
||||
fn admin_monitoring_headers_contain_trace_id(
|
||||
headers: Option<&Value>,
|
||||
trace_request_id: &str,
|
||||
) -> bool {
|
||||
headers.and_then(Value::as_object).is_some_and(|object| {
|
||||
object.iter().any(|(key, value)| {
|
||||
key.eq_ignore_ascii_case("x-trace-id")
|
||||
&& value
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| value == trace_request_id)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_monitoring_candidate_status_rank(status: RequestCandidateStatus) -> u8 {
|
||||
match status {
|
||||
RequestCandidateStatus::Success => 7,
|
||||
|
||||
@@ -1093,10 +1093,58 @@ fn infer_client_family_from_user_agent(user_agent: &str) -> Option<&'static str>
|
||||
if normalized.contains("geminicli") || normalized.contains("gemini-cli") {
|
||||
return Some("gemini_cli");
|
||||
}
|
||||
if normalized.contains("qwencode") {
|
||||
return Some("qwen_code");
|
||||
}
|
||||
if normalized.contains("roo-code") || normalized.contains("roocode") {
|
||||
return Some("roo_code");
|
||||
}
|
||||
if normalized.contains("kilo-code") || normalized.contains("kilocode") {
|
||||
return Some("kilocode");
|
||||
}
|
||||
if normalized.contains("cherrystudio") || normalized.contains("cherry-studio") {
|
||||
return Some("cherrystudio");
|
||||
}
|
||||
if normalized.contains("openui-agent-manager") || normalized.contains("openui") {
|
||||
return Some("openui");
|
||||
}
|
||||
if normalized.contains("cursor") {
|
||||
return Some("cursor");
|
||||
}
|
||||
if normalized.contains("windsurf") {
|
||||
return Some("windsurf");
|
||||
}
|
||||
if normalized.contains("continue") {
|
||||
return Some("continue");
|
||||
}
|
||||
if normalized.contains("cline") {
|
||||
return Some("cline");
|
||||
}
|
||||
if normalized.contains("aider") {
|
||||
return Some("aider");
|
||||
}
|
||||
if normalized.contains("langchain") {
|
||||
return Some("langchain");
|
||||
}
|
||||
if normalized.contains("llamaindex") || normalized.contains("llama-index") {
|
||||
return Some("llamaindex");
|
||||
}
|
||||
if normalized.starts_with("openai/js") {
|
||||
return Some("openai_js_sdk");
|
||||
}
|
||||
None
|
||||
if normalized.starts_with("openai/python") {
|
||||
return Some("openai_python_sdk");
|
||||
}
|
||||
if normalized.starts_with("anthropic/js") || normalized.contains("anthropic-sdk-typescript") {
|
||||
return Some("anthropic_js_sdk");
|
||||
}
|
||||
if normalized.starts_with("anthropic/python") || normalized.contains("anthropic-sdk-python") {
|
||||
return Some("anthropic_python_sdk");
|
||||
}
|
||||
if normalized.contains("/js ") || normalized.contains("/python ") {
|
||||
return Some("sdk");
|
||||
}
|
||||
Some("unknown")
|
||||
}
|
||||
|
||||
pub fn admin_usage_client_family(item: &StoredRequestUsageAudit) -> Option<&str> {
|
||||
@@ -1174,6 +1222,12 @@ fn admin_usage_active_request_json(
|
||||
if let Some(target_model) = item.target_model.as_ref() {
|
||||
value["target_model"] = json!(target_model);
|
||||
}
|
||||
if let Some(reasoning_effort) = item.provider_reasoning_effort() {
|
||||
value["reasoning_effort"] = json!(reasoning_effort);
|
||||
}
|
||||
if let Some(service_tier) = item.provider_service_tier() {
|
||||
value["service_tier"] = json!(service_tier);
|
||||
}
|
||||
if let Some(image_progress) = image_progress {
|
||||
value["image_progress"] = image_progress.clone();
|
||||
}
|
||||
@@ -1287,6 +1341,12 @@ pub fn admin_usage_record_json(
|
||||
"request_path_and_query",
|
||||
admin_usage_metadata_string(item, "request_path_and_query"),
|
||||
);
|
||||
if let Some(reasoning_effort) = item.provider_reasoning_effort() {
|
||||
object.insert("reasoning_effort".to_string(), json!(reasoning_effort));
|
||||
}
|
||||
if let Some(service_tier) = item.provider_service_tier() {
|
||||
object.insert("service_tier".to_string(), json!(service_tier));
|
||||
}
|
||||
payload
|
||||
}
|
||||
|
||||
@@ -2606,6 +2666,32 @@ mod tests {
|
||||
assert_eq!(record["client_family"], "codex");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_usage_record_includes_provider_reasoning_effort() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
provider_request_body: Some(json!({
|
||||
"reasoning": { "effort": "xhigh" },
|
||||
"service_tier": "priority"
|
||||
})),
|
||||
..sample_usage("completed", Some(200), None)
|
||||
};
|
||||
|
||||
let record = admin_usage_record_json(
|
||||
&item,
|
||||
&BTreeMap::new(),
|
||||
&BTreeMap::new(),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
let active = admin_usage_active_request_json(&item, None, None, None);
|
||||
|
||||
assert_eq!(record["reasoning_effort"], "xhigh");
|
||||
assert_eq!(active["reasoning_effort"], "xhigh");
|
||||
assert_eq!(record["service_tier"], "priority");
|
||||
assert_eq!(active["service_tier"], "priority");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_requested_stream_uses_stream_generate_content_path_over_stale_metadata_flag() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
|
||||
@@ -440,7 +440,21 @@ pub fn admin_provider_ops_verify_headers(
|
||||
)?;
|
||||
}
|
||||
}
|
||||
"nekocode" | "done_hub" => {
|
||||
"nekocode" => {
|
||||
if let Some(session_cookie) = credentials
|
||||
.get("session_cookie")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
insert_header(
|
||||
&mut headers,
|
||||
"Cookie",
|
||||
&admin_provider_ops_session_cookie_header(session_cookie),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
"done_hub" => {
|
||||
insert_header(&mut headers, "User-Agent", ADMIN_PROVIDER_OPS_USER_AGENT)?;
|
||||
if let Some(session_cookie) = credentials
|
||||
.get("session_cookie")
|
||||
.and_then(Value::as_str)
|
||||
@@ -1005,6 +1019,12 @@ mod tests {
|
||||
headers.get(COOKIE).and_then(|value| value.to_str().ok()),
|
||||
Some("session=abc")
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(USER_AGENT)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some(ADMIN_PROVIDER_OPS_USER_AGENT)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -157,18 +157,39 @@ fn admin_pool_health_score(key: &StoredProviderCatalogKey) -> f64 {
|
||||
}
|
||||
|
||||
fn admin_pool_circuit_breaker_open(key: &StoredProviderCatalogKey) -> bool {
|
||||
let now_unix_secs = Utc::now().timestamp().max(0) as u64;
|
||||
key.circuit_breaker_by_format
|
||||
.as_ref()
|
||||
.and_then(Value::as_object)
|
||||
.map(|formats| {
|
||||
formats
|
||||
.values()
|
||||
.filter_map(Value::as_object)
|
||||
.any(|item| item.get("open").and_then(Value::as_bool).unwrap_or(false))
|
||||
.any(|item| admin_pool_circuit_payload_active_open_at(item, now_unix_secs))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn admin_pool_circuit_payload_active_open_at(value: &Value, now_unix_secs: u64) -> bool {
|
||||
let Some(item) = value.as_object() else {
|
||||
return false;
|
||||
};
|
||||
if !item.get("open").and_then(Value::as_bool).unwrap_or(false) {
|
||||
return false;
|
||||
}
|
||||
if let Some(next_probe_at) = item.get("next_probe_at_unix_secs").and_then(Value::as_u64) {
|
||||
return now_unix_secs < next_probe_at;
|
||||
}
|
||||
if let Some(next_probe_at) = item
|
||||
.get("next_probe_at")
|
||||
.and_then(Value::as_str)
|
||||
.and_then(|value| chrono::DateTime::parse_from_rfc3339(value).ok())
|
||||
.and_then(|value| u64::try_from(value.timestamp()).ok())
|
||||
{
|
||||
return now_unix_secs < next_probe_at;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn unix_secs_to_rfc3339(unix_secs: u64) -> Option<String> {
|
||||
Utc.timestamp_opt(unix_secs as i64, 0)
|
||||
.single()
|
||||
|
||||
@@ -199,7 +199,7 @@ pub fn quota_refresh_success_invalid_state(
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or_default();
|
||||
if current_reason.starts_with(OAUTH_REFRESH_FAILED_PREFIX) {
|
||||
if current_reason.starts_with(OAUTH_ACCOUNT_BLOCK_PREFIX) {
|
||||
return (
|
||||
key.oauth_invalid_at_unix_secs,
|
||||
(!current_reason.is_empty()).then_some(current_reason.to_string()),
|
||||
@@ -1724,6 +1724,7 @@ mod tests {
|
||||
parse_codex_wham_usage_response, parse_gemini_cli_retrieve_user_quota_response,
|
||||
parse_gemini_cli_v1internal_credits_response, parse_windsurf_model_configs_response,
|
||||
parse_windsurf_rate_limit_response, parse_windsurf_user_status_response,
|
||||
quota_refresh_success_invalid_state, should_auto_remove_structured_reason,
|
||||
OAUTH_ACCOUNT_BLOCK_PREFIX, OAUTH_EXPIRED_PREFIX, OAUTH_REFRESH_FAILED_PREFIX,
|
||||
OAUTH_REQUEST_FAILED_PREFIX,
|
||||
};
|
||||
@@ -1846,6 +1847,13 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_remove_structured_reason_keeps_oauth_expired_token_invalid() {
|
||||
assert!(!should_auto_remove_structured_reason(Some(
|
||||
"[OAUTH_EXPIRED] token invalidated"
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_remove_refresh_failed_after_access_token_expiry() {
|
||||
let mut key = StoredProviderCatalogKey::new(
|
||||
@@ -1996,6 +2004,32 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quota_refresh_success_clears_refresh_failed_marker() {
|
||||
let mut key = StoredProviderCatalogKey::new(
|
||||
"key-1".to_string(),
|
||||
"provider-1".to_string(),
|
||||
"key-1".to_string(),
|
||||
"oauth".to_string(),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("key should build");
|
||||
key.oauth_invalid_reason = Some("[REFRESH_FAILED] Token 续期失败".to_string());
|
||||
|
||||
assert_eq!(quota_refresh_success_invalid_state(&key), (None, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_remove_structured_reason_keeps_request_and_refresh_failures() {
|
||||
assert!(!should_auto_remove_structured_reason(Some(
|
||||
"[REQUEST_FAILED] 账号状态检查失败"
|
||||
)));
|
||||
assert!(!should_auto_remove_structured_reason(Some(
|
||||
"[REFRESH_FAILED] Token 续期失败 (401): refresh_token 已失效"
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_codex_spark_quota_from_additional_rate_limits() {
|
||||
let parsed = parse_codex_wham_usage_response(
|
||||
|
||||
@@ -644,6 +644,19 @@ mod tests {
|
||||
assert!(!snapshot.recoverable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_expired_state_is_not_auto_removed() {
|
||||
let state = resolve_pool_account_state(
|
||||
Some("codex"),
|
||||
None,
|
||||
Some("[OAUTH_EXPIRED] token invalidated"),
|
||||
);
|
||||
|
||||
assert!(state.blocked);
|
||||
assert_eq!(state.code.as_deref(), Some("oauth_token_invalid"));
|
||||
assert!(!should_auto_remove_account_state(&state));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_snapshot_detects_account_block_and_verification() {
|
||||
let snapshot = resolve_account_status_snapshot(
|
||||
|
||||
@@ -43,12 +43,12 @@ pub struct AdminEmailTemplateUpdate {
|
||||
pub html: Option<String>,
|
||||
}
|
||||
|
||||
pub const ADMIN_SYSTEM_CONFIG_EXPORT_VERSION: &str = "2.2";
|
||||
pub const ADMIN_SYSTEM_CONFIG_EXPORT_VERSION: &str = "2.3";
|
||||
pub const ADMIN_SYSTEM_CONFIG_SUPPORTED_VERSIONS: &[&str] =
|
||||
&["2.0", "2.1", ADMIN_SYSTEM_CONFIG_EXPORT_VERSION];
|
||||
pub const ADMIN_SYSTEM_USERS_EXPORT_VERSION: &str = "1.4";
|
||||
&["2.0", "2.1", "2.2", ADMIN_SYSTEM_CONFIG_EXPORT_VERSION];
|
||||
pub const ADMIN_SYSTEM_USERS_EXPORT_VERSION: &str = "1.5";
|
||||
pub const ADMIN_SYSTEM_USERS_SUPPORTED_VERSIONS: &[&str] =
|
||||
&["1.3", ADMIN_SYSTEM_USERS_EXPORT_VERSION];
|
||||
&["1.3", "1.4", ADMIN_SYSTEM_USERS_EXPORT_VERSION];
|
||||
pub const ADMIN_SYSTEM_PROVIDER_OPS_SENSITIVE_CREDENTIAL_FIELDS: &[&str] = &[
|
||||
"api_key",
|
||||
"password",
|
||||
@@ -67,6 +67,8 @@ pub struct AdminSystemUpdateRelease {
|
||||
pub release_url: Option<String>,
|
||||
pub release_notes: Option<String>,
|
||||
pub published_at: Option<String>,
|
||||
pub tarball_url: Option<String>,
|
||||
pub sha256sums_url: Option<String>,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
@@ -269,6 +271,28 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_optional_u64_from_number<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = Option::<Value>::deserialize(deserializer)?;
|
||||
match value {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(Value::Number(number)) => number
|
||||
.as_u64()
|
||||
.map(Some)
|
||||
.ok_or_else(|| de::Error::custom("expected a non-negative integer or numeric string")),
|
||||
Some(Value::String(raw)) if !raw.trim().is_empty() => raw
|
||||
.trim()
|
||||
.parse::<u64>()
|
||||
.map(Some)
|
||||
.map_err(|_| de::Error::custom("expected a non-negative integer or numeric string")),
|
||||
Some(_) => Err(de::Error::custom(
|
||||
"expected a non-negative integer or numeric string",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AdminImportMergeMode {
|
||||
@@ -345,6 +369,8 @@ pub struct AdminSystemConfigImportStats {
|
||||
pub struct AdminSystemConfigGlobalModel {
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_u64_from_number")]
|
||||
pub usage_count: Option<u64>,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_f64_from_number")]
|
||||
pub default_price_per_request: Option<f64>,
|
||||
#[serde(default)]
|
||||
@@ -709,6 +735,7 @@ const DEFAULT_BARK_API_BASE: &str = "https://api.day.app";
|
||||
const SENSITIVE_SYSTEM_CONFIG_KEYS: &[&str] = &[
|
||||
"smtp_password",
|
||||
"turnstile_secret_key",
|
||||
"backup_s3_secret_access_key",
|
||||
"module.server_chan_push.send_key",
|
||||
"module.important_notification.server_chan_send_key",
|
||||
"module.bark_push.device_key",
|
||||
@@ -826,11 +853,19 @@ pub fn build_admin_system_check_update_payload_with_release(
|
||||
let has_update = latest_release
|
||||
.as_ref()
|
||||
.is_some_and(|release| admin_system_update_available(¤t_version, &release.version));
|
||||
let update_blocker = latest_release
|
||||
.as_ref()
|
||||
.and_then(admin_system_update_blocker);
|
||||
let updatable = latest_release
|
||||
.as_ref()
|
||||
.is_some_and(|release| admin_system_update_blocker(release).is_none());
|
||||
|
||||
json!({
|
||||
"current_version": current_version,
|
||||
"latest_version": latest_release.as_ref().map(|release| release.version.clone()),
|
||||
"has_update": has_update,
|
||||
"updatable": updatable,
|
||||
"update_blocker": update_blocker,
|
||||
"release_url": latest_release.as_ref().and_then(|release| release.release_url.clone()),
|
||||
"release_notes": latest_release.as_ref().and_then(|release| release.release_notes.clone()),
|
||||
"published_at": latest_release.as_ref().and_then(|release| release.published_at.clone()),
|
||||
@@ -838,6 +873,53 @@ pub fn build_admin_system_check_update_payload_with_release(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_admin_system_releases_payload(
|
||||
current_version: String,
|
||||
releases: Vec<AdminSystemUpdateRelease>,
|
||||
error: Option<String>,
|
||||
) -> serde_json::Value {
|
||||
let entries: Vec<serde_json::Value> = releases
|
||||
.iter()
|
||||
.map(|release| {
|
||||
let is_current = {
|
||||
let norm_current = normalized_admin_system_version(¤t_version);
|
||||
let norm_release = normalized_admin_system_version(&release.version);
|
||||
norm_current == norm_release
|
||||
};
|
||||
let is_newer = admin_system_update_available(¤t_version, &release.version);
|
||||
let update_blocker = admin_system_update_blocker(release);
|
||||
json!({
|
||||
"version": release.version,
|
||||
"release_url": release.release_url,
|
||||
"release_notes": release.release_notes,
|
||||
"published_at": release.published_at,
|
||||
"tarball_url": release.tarball_url,
|
||||
"sha256sums_url": release.sha256sums_url,
|
||||
"is_current": is_current,
|
||||
"is_newer": is_newer,
|
||||
"updatable": update_blocker.is_none(),
|
||||
"update_blocker": update_blocker,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
json!({
|
||||
"current_version": current_version,
|
||||
"releases": entries,
|
||||
"error": error,
|
||||
})
|
||||
}
|
||||
|
||||
fn admin_system_update_blocker(release: &AdminSystemUpdateRelease) -> Option<&'static str> {
|
||||
if release.tarball_url.is_none() {
|
||||
Some("当前平台暂无安装包")
|
||||
} else if release.sha256sums_url.is_none() {
|
||||
Some("缺少 SHA256SUMS 校验文件")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized_admin_system_version(version: &str) -> String {
|
||||
let trimmed = version.trim();
|
||||
trimmed
|
||||
@@ -1202,15 +1284,31 @@ pub fn ldap_module_config_is_valid(config: Option<&StoredLdapModuleConfig>) -> b
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub struct AdminModuleValidationInput<'a> {
|
||||
pub module_name: &'a str,
|
||||
pub oauth_providers: &'a [StoredOAuthProviderModuleConfig],
|
||||
pub ldap_config: Option<&'a StoredLdapModuleConfig>,
|
||||
pub gemini_files_has_capable_key: bool,
|
||||
pub important_notification_configured: bool,
|
||||
pub server_chan_push_configured: bool,
|
||||
pub bark_push_configured: bool,
|
||||
pub s3_backup_configured: bool,
|
||||
}
|
||||
|
||||
pub fn build_admin_module_validation_result(
|
||||
module_name: &str,
|
||||
oauth_providers: &[StoredOAuthProviderModuleConfig],
|
||||
ldap_config: Option<&StoredLdapModuleConfig>,
|
||||
gemini_files_has_capable_key: bool,
|
||||
important_notification_configured: bool,
|
||||
server_chan_push_configured: bool,
|
||||
bark_push_configured: bool,
|
||||
input: AdminModuleValidationInput<'_>,
|
||||
) -> (bool, Option<String>) {
|
||||
let AdminModuleValidationInput {
|
||||
module_name,
|
||||
oauth_providers,
|
||||
ldap_config,
|
||||
gemini_files_has_capable_key,
|
||||
important_notification_configured,
|
||||
server_chan_push_configured,
|
||||
bark_push_configured,
|
||||
s3_backup_configured,
|
||||
} = input;
|
||||
|
||||
match module_name {
|
||||
"oauth" => {
|
||||
if oauth_providers.is_empty() {
|
||||
@@ -1301,6 +1399,13 @@ pub fn build_admin_module_validation_result(
|
||||
(false, Some("请先配置 Bark Device Key".to_string()))
|
||||
}
|
||||
}
|
||||
"s3_backup" => {
|
||||
if s3_backup_configured {
|
||||
(true, None)
|
||||
} else {
|
||||
(false, Some("请先完成 S3 备份配置".to_string()))
|
||||
}
|
||||
}
|
||||
"gemini_files" => {
|
||||
if gemini_files_has_capable_key {
|
||||
(true, None)
|
||||
@@ -1326,7 +1431,8 @@ pub fn build_admin_module_health(
|
||||
| "proxy_nodes"
|
||||
| "important_notification"
|
||||
| "bark_push"
|
||||
| "server_chan_push" => "healthy",
|
||||
| "server_chan_push"
|
||||
| "s3_backup" => "healthy",
|
||||
"gemini_files" => {
|
||||
if gemini_files_has_capable_key {
|
||||
"healthy"
|
||||
@@ -1590,6 +1696,24 @@ pub fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value>
|
||||
"turnstile_site_key" => Some(serde_json::Value::Null),
|
||||
"turnstile_secret_key" => Some(serde_json::Value::Null),
|
||||
"turnstile_allowed_hostnames" => Some(json!([])),
|
||||
"backup_s3_enabled" => Some(json!(false)),
|
||||
"backup_s3_scope" => Some(json!("data")),
|
||||
"backup_s3_endpoint" => Some(serde_json::Value::Null),
|
||||
"backup_s3_region" => Some(json!("auto")),
|
||||
"backup_s3_bucket" => Some(serde_json::Value::Null),
|
||||
"backup_s3_prefix" => Some(json!("aether/backups/")),
|
||||
"backup_s3_access_key_id" => Some(serde_json::Value::Null),
|
||||
"backup_s3_secret_access_key" => Some(serde_json::Value::Null),
|
||||
"backup_s3_path_style" => Some(json!(true)),
|
||||
"backup_s3_compression" => Some(json!("zstd")),
|
||||
"backup_s3_schedule_unit" => Some(json!("days")),
|
||||
"backup_s3_schedule_interval" => Some(json!(1)),
|
||||
"backup_s3_schedule_minute" => Some(json!(0)),
|
||||
"backup_s3_schedule_hour" => Some(json!(3)),
|
||||
"backup_s3_schedule_weekday" => Some(json!(1)),
|
||||
"backup_s3_schedule_month_day" => Some(json!(1)),
|
||||
"backup_s3_retention_count" => Some(json!(7)),
|
||||
"backup_s3_last_slot" => Some(serde_json::Value::Null),
|
||||
"email_suffix_mode" => Some(json!("none")),
|
||||
"email_suffix_list" => Some(json!([])),
|
||||
"enable_format_conversion" => Some(json!(false)),
|
||||
@@ -1605,7 +1729,8 @@ pub fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value>
|
||||
"medium": { "reasoning_effort": "medium" },
|
||||
"high": { "reasoning_effort": "high" },
|
||||
"xhigh": { "reasoning_effort": "xhigh" },
|
||||
"max": { "reasoning_effort": "xhigh" }
|
||||
"max": { "reasoning_effort": "xhigh" },
|
||||
"fast": { "service_tier": "priority" }
|
||||
}
|
||||
},
|
||||
"openai:responses": {
|
||||
@@ -1615,7 +1740,8 @@ pub fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value>
|
||||
"medium": { "reasoning": { "effort": "medium" } },
|
||||
"high": { "reasoning": { "effort": "high" } },
|
||||
"xhigh": { "reasoning": { "effort": "xhigh" } },
|
||||
"max": { "reasoning": { "effort": "xhigh" } }
|
||||
"max": { "reasoning": { "effort": "xhigh" } },
|
||||
"fast": { "service_tier": "priority" }
|
||||
}
|
||||
},
|
||||
"openai:responses:compact": {
|
||||
@@ -1625,7 +1751,8 @@ pub fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value>
|
||||
"medium": { "reasoning": { "effort": "medium" } },
|
||||
"high": { "reasoning": { "effort": "high" } },
|
||||
"xhigh": { "reasoning": { "effort": "xhigh" } },
|
||||
"max": { "reasoning": { "effort": "xhigh" } }
|
||||
"max": { "reasoning": { "effort": "xhigh" } },
|
||||
"fast": { "service_tier": "priority" }
|
||||
}
|
||||
},
|
||||
"claude:messages": {
|
||||
@@ -2950,6 +3077,8 @@ mod tests {
|
||||
),
|
||||
release_notes: Some("release notes".to_string()),
|
||||
published_at: Some("2026-05-13T00:00:00Z".to_string()),
|
||||
tarball_url: None,
|
||||
sha256sums_url: None,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
@@ -2963,9 +3092,31 @@ mod tests {
|
||||
);
|
||||
assert_eq!(payload["release_notes"], "release notes");
|
||||
assert_eq!(payload["published_at"], "2026-05-13T00:00:00Z");
|
||||
assert_eq!(payload["updatable"], false);
|
||||
assert_eq!(payload["update_blocker"], "当前平台暂无安装包");
|
||||
assert_eq!(payload["error"], serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_admin_system_check_update_payload_reports_updatable_release() {
|
||||
let payload = build_admin_system_check_update_payload_with_release(
|
||||
"0.7.0-rc27".to_string(),
|
||||
Some(AdminSystemUpdateRelease {
|
||||
version: "v0.7.0-rc28".to_string(),
|
||||
release_url: None,
|
||||
release_notes: None,
|
||||
published_at: None,
|
||||
tarball_url: Some("https://github.com/fawney19/Aether/releases/download/v0.7.0-rc28/aether.tar.gz".to_string()),
|
||||
sha256sums_url: Some("https://github.com/fawney19/Aether/releases/download/v0.7.0-rc28/SHA256SUMS".to_string()),
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(payload["has_update"], true);
|
||||
assert_eq!(payload["updatable"], true);
|
||||
assert_eq!(payload["update_blocker"], serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_admin_system_check_update_payload_normalizes_v_prefix() {
|
||||
let payload = build_admin_system_check_update_payload_with_release(
|
||||
@@ -2975,6 +3126,8 @@ mod tests {
|
||||
release_url: None,
|
||||
release_notes: None,
|
||||
published_at: None,
|
||||
tarball_url: None,
|
||||
sha256sums_url: None,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
@@ -2992,6 +3145,8 @@ mod tests {
|
||||
release_url: None,
|
||||
release_notes: None,
|
||||
published_at: None,
|
||||
tarball_url: None,
|
||||
sha256sums_url: None,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
@@ -3009,6 +3164,8 @@ mod tests {
|
||||
release_url: None,
|
||||
release_notes: None,
|
||||
published_at: None,
|
||||
tarball_url: None,
|
||||
sha256sums_url: None,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
@@ -3026,6 +3183,8 @@ mod tests {
|
||||
release_url: None,
|
||||
release_notes: None,
|
||||
published_at: None,
|
||||
tarball_url: None,
|
||||
sha256sums_url: None,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
@@ -3058,7 +3217,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_admin_system_config_import_request_rejects_unknown_versions() {
|
||||
for version in ["1.9", "2.3"] {
|
||||
for version in ["1.9", "2.4"] {
|
||||
let err = parse_admin_system_config_import_request(
|
||||
json!({
|
||||
"version": version,
|
||||
@@ -3134,6 +3293,7 @@ mod tests {
|
||||
"global_models": [{
|
||||
"name": "veo3.1",
|
||||
"display_name": "Veo 3.1",
|
||||
"usage_count": "42",
|
||||
"default_price_per_request": "1.80000000",
|
||||
}],
|
||||
"providers": [{
|
||||
@@ -3154,6 +3314,7 @@ mod tests {
|
||||
.expect("numeric string fields from Python exports should parse");
|
||||
|
||||
let global_model = &parsed.request.document.global_models[0];
|
||||
assert_eq!(global_model.usage_count, Some(42));
|
||||
assert_eq!(global_model.default_price_per_request, Some(1.8));
|
||||
|
||||
let provider = &parsed.request.document.providers[0];
|
||||
@@ -3215,6 +3376,41 @@ mod tests {
|
||||
assert!(!is_sensitive_admin_system_config_key("site_name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s3_backup_secret_access_key_is_sensitive() {
|
||||
assert!(is_sensitive_admin_system_config_key(
|
||||
"backup_s3_secret_access_key"
|
||||
));
|
||||
assert!(is_sensitive_admin_system_config_key(
|
||||
"BACKUP_S3_SECRET_ACCESS_KEY"
|
||||
));
|
||||
assert!(!is_sensitive_admin_system_config_key("backup_s3_bucket"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s3_backup_defaults_match_admin_ui_contract() {
|
||||
assert_eq!(
|
||||
admin_system_config_default_value("backup_s3_scope"),
|
||||
Some(json!("data"))
|
||||
);
|
||||
assert_eq!(
|
||||
admin_system_config_default_value("backup_s3_schedule_unit"),
|
||||
Some(json!("days"))
|
||||
);
|
||||
assert_eq!(
|
||||
admin_system_config_default_value("backup_s3_schedule_interval"),
|
||||
Some(json!(1))
|
||||
);
|
||||
assert_eq!(
|
||||
admin_system_config_default_value("backup_s3_retention_count"),
|
||||
Some(json!(7))
|
||||
);
|
||||
assert_eq!(
|
||||
admin_system_config_default_value("backup_s3_path_style"),
|
||||
Some(json!(true))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_notification_email_config_key_normalizes_to_important_notification() {
|
||||
assert_eq!(
|
||||
@@ -3230,6 +3426,19 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s3_backup_secret_detail_is_write_only() {
|
||||
let payload = build_admin_system_config_detail_payload(
|
||||
"backup_s3_secret_access_key",
|
||||
Some(json!("encrypted-secret")),
|
||||
)
|
||||
.expect("sensitive backup key should render");
|
||||
|
||||
assert_eq!(payload["key"], json!("backup_s3_secret_access_key"));
|
||||
assert_eq!(payload["value"], serde_json::Value::Null);
|
||||
assert_eq!(payload["is_set"], json!(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_server_chan_config_keys_normalize_to_push_module() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user