feat: 增强自动加分规则管理,支持规则的创建、更新、删除和持久化

This commit is contained in:
NanGua-QWQ
2026-03-30 19:12:03 +08:00
parent e4d9ee0c85
commit 9c3e84d912
5 changed files with 490 additions and 250 deletions
+160 -152
View File
@@ -1,10 +1,12 @@
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::sync::Arc;
use tauri::{AppHandle, Emitter, State};
use crate::services::{
AutoScoreAction, AutoScoreRule, AutoScoreTrigger, PermissionLevel, SettingsKey, SettingsValue,
AutoScoreAction, AutoScoreRule, AutoScoreService, AutoScoreTrigger, PermissionLevel,
SettingsKey, SettingsValue,
};
use crate::state::AppState;
@@ -51,6 +53,101 @@ pub struct AutoScoreStatus {
pub enabled: bool,
}
fn build_rule_from_create(rule: CreateAutoScoreRule) -> AutoScoreRule {
AutoScoreRule {
id: 0,
name: rule.name,
enabled: rule.enabled,
student_names: rule.student_names,
triggers: rule.triggers,
actions: rule.actions,
last_executed: None,
}
}
fn build_rule_from_update(rule: UpdateAutoScoreRule) -> AutoScoreRule {
AutoScoreRule {
id: rule.id,
name: rule.name,
enabled: rule.enabled,
student_names: rule.student_names,
triggers: rule.triggers,
actions: rule.actions,
last_executed: None,
}
}
async fn load_rules_from_settings(
state: &Arc<RwLock<AppState>>,
) -> Result<Vec<AutoScoreRule>, String> {
let state_guard = state.read();
let db_conn = state_guard.db.read().clone();
let mut settings = state_guard.settings.write();
settings.attach_db(db_conn);
settings.initialize().await?;
let rules_json = match settings.get_value(SettingsKey::AutoScoreRules) {
SettingsValue::Json(value) => value,
_ => JsonValue::Array(vec![]),
};
Ok(AutoScoreService::deserialize_rules(&rules_json))
}
async fn persist_rules_to_settings(
state: &Arc<RwLock<AppState>>,
rules: &[AutoScoreRule],
) -> Result<(), String> {
let rules_json = AutoScoreService::serialize_rules(rules)?;
let enabled = rules.iter().any(|rule| rule.enabled);
let state_guard = state.read();
let db_conn = state_guard.db.read().clone();
let mut settings = state_guard.settings.write();
settings.attach_db(db_conn);
settings.initialize().await?;
settings
.set_value(SettingsKey::AutoScoreRules, SettingsValue::Json(rules_json))
.await?;
settings
.set_value(
SettingsKey::AutoScoreEnabled,
SettingsValue::Boolean(enabled),
)
.await?;
Ok(())
}
fn replace_cached_rules(state: &Arc<RwLock<AppState>>, rules: Vec<AutoScoreRule>) {
let state_guard = state.read();
let mut auto_score_service = state_guard.auto_score.write();
auto_score_service.replace_rules(rules);
}
async fn sync_cached_rules(state: &Arc<RwLock<AppState>>) -> Result<Vec<AutoScoreRule>, String> {
let rules = load_rules_from_settings(state).await?;
replace_cached_rules(state, rules.clone());
Ok(rules)
}
fn emit_rules_changed(app_handle: &AppHandle, state: &Arc<RwLock<AppState>>) {
let state_guard = state.read();
let rules = state_guard.auto_score.read().get_rules().to_vec();
let _ = app_handle.emit("auto-score:rulesChanged", &rules);
}
fn emit_auto_score_status_changed(app_handle: &AppHandle, rules: &[AutoScoreRule]) {
let enabled = rules.iter().any(|rule| rule.enabled);
let _ = app_handle.emit(
"settings:changed",
serde_json::json!({
"key": "auto_score_enabled",
"value": enabled,
}),
);
}
#[tauri::command]
pub async fn auto_score_get_rules(
sender_id: Option<u32>,
@@ -64,9 +161,7 @@ pub async fn auto_score_get_rules(
}
}
let state_guard = state.read();
let auto_score_service = state_guard.auto_score.read();
let rules = auto_score_service.get_rules().to_vec();
let rules = sync_cached_rules(state.inner()).await?;
Ok(IpcResponse::success(rules))
}
@@ -85,39 +180,19 @@ pub async fn auto_score_add_rule(
}
}
let new_id = {
let state_guard = state.read();
let db_conn = state_guard.db.read().clone();
let mut auto_score_service = state_guard.auto_score.write();
let mut settings = state_guard.settings.write();
settings.attach_db(db_conn);
settings.initialize().await?;
let new_rule = AutoScoreRule {
id: 0,
name: rule.name,
enabled: rule.enabled,
student_names: rule.student_names,
triggers: rule.triggers,
actions: rule.actions,
last_executed: None,
};
let new_id = auto_score_service.add_rule(new_rule);
let rules_json = auto_score_service.get_rules_json();
let _ = settings
.set_value(SettingsKey::AutoScoreRules, SettingsValue::Json(rules_json))
.await;
new_id
let current_rules = sync_cached_rules(state.inner()).await?;
let mut working_service = AutoScoreService::from_rules(current_rules);
let new_id = match working_service.add_rule(build_rule_from_create(rule)) {
Ok(value) => value,
Err(message) => return Ok(IpcResponse::error(&message)),
};
{
let state_guard = state.read();
let auto_score_service = state_guard.auto_score.read();
let _ = app_handle.emit("auto-score:rulesChanged", &auto_score_service.get_rules());
}
let next_rules = working_service.into_rules();
persist_rules_to_settings(state.inner(), &next_rules).await?;
replace_cached_rules(state.inner(), next_rules);
let rules = state.read().auto_score.read().get_rules().to_vec();
emit_auto_score_status_changed(&app_handle, &rules);
emit_rules_changed(&app_handle, state.inner());
Ok(IpcResponse::success(new_id))
}
@@ -137,47 +212,20 @@ pub async fn auto_score_update_rule(
}
}
let success = {
let state_guard = state.read();
let db_conn = state_guard.db.read().clone();
let mut auto_score_service = state_guard.auto_score.write();
let mut settings = state_guard.settings.write();
settings.attach_db(db_conn);
settings.initialize().await?;
let existing = auto_score_service.get_rule_by_id(rule.id);
let last_executed = existing.and_then(|r| r.last_executed.clone());
let updated_rule = AutoScoreRule {
id: rule.id,
name: rule.name,
enabled: rule.enabled,
student_names: rule.student_names,
triggers: rule.triggers,
actions: rule.actions,
last_executed,
};
let success = auto_score_service.update_rule(updated_rule);
if success {
let rules_json = auto_score_service.get_rules_json();
let _ = settings
.set_value(SettingsKey::AutoScoreRules, SettingsValue::Json(rules_json))
.await;
}
success
};
if success {
let state_guard = state.read();
let auto_score_service = state_guard.auto_score.read();
let _ = app_handle.emit("auto-score:rulesChanged", &auto_score_service.get_rules());
Ok(IpcResponse::success(true))
} else {
Ok(IpcResponse::error("Rule not found"))
let current_rules = sync_cached_rules(state.inner()).await?;
let mut working_service = AutoScoreService::from_rules(current_rules);
if let Err(message) = working_service.update_rule(build_rule_from_update(rule)) {
return Ok(IpcResponse::error(&message));
}
let next_rules = working_service.into_rules();
persist_rules_to_settings(state.inner(), &next_rules).await?;
replace_cached_rules(state.inner(), next_rules);
let rules = state.read().auto_score.read().get_rules().to_vec();
emit_auto_score_status_changed(&app_handle, &rules);
emit_rules_changed(&app_handle, state.inner());
Ok(IpcResponse::success(true))
}
#[tauri::command]
@@ -195,34 +243,20 @@ pub async fn auto_score_delete_rule(
}
}
let success = {
let state_guard = state.read();
let db_conn = state_guard.db.read().clone();
let mut auto_score_service = state_guard.auto_score.write();
let mut settings = state_guard.settings.write();
settings.attach_db(db_conn);
settings.initialize().await?;
let success = auto_score_service.delete_rule(rule_id);
if success {
let rules_json = auto_score_service.get_rules_json();
let _ = settings
.set_value(SettingsKey::AutoScoreRules, SettingsValue::Json(rules_json))
.await;
}
success
};
if success {
let state_guard = state.read();
let auto_score_service = state_guard.auto_score.read();
let _ = app_handle.emit("auto-score:rulesChanged", &auto_score_service.get_rules());
Ok(IpcResponse::success(true))
} else {
Ok(IpcResponse::error("Rule not found"))
let current_rules = sync_cached_rules(state.inner()).await?;
let mut working_service = AutoScoreService::from_rules(current_rules);
if let Err(message) = working_service.delete_rule(rule_id) {
return Ok(IpcResponse::error(&message));
}
let next_rules = working_service.into_rules();
persist_rules_to_settings(state.inner(), &next_rules).await?;
replace_cached_rules(state.inner(), next_rules);
let rules = state.read().auto_score.read().get_rules().to_vec();
emit_auto_score_status_changed(&app_handle, &rules);
emit_rules_changed(&app_handle, state.inner());
Ok(IpcResponse::success(true))
}
#[tauri::command]
@@ -240,34 +274,20 @@ pub async fn auto_score_toggle_rule(
}
}
let success = {
let state_guard = state.read();
let db_conn = state_guard.db.read().clone();
let mut auto_score_service = state_guard.auto_score.write();
let mut settings = state_guard.settings.write();
settings.attach_db(db_conn);
settings.initialize().await?;
let success = auto_score_service.toggle_rule(params.rule_id, params.enabled);
if success {
let rules_json = auto_score_service.get_rules_json();
let _ = settings
.set_value(SettingsKey::AutoScoreRules, SettingsValue::Json(rules_json))
.await;
}
success
};
if success {
let state_guard = state.read();
let auto_score_service = state_guard.auto_score.read();
let _ = app_handle.emit("auto-score:rulesChanged", &auto_score_service.get_rules());
Ok(IpcResponse::success(true))
} else {
Ok(IpcResponse::error("Rule not found"))
let current_rules = sync_cached_rules(state.inner()).await?;
let mut working_service = AutoScoreService::from_rules(current_rules);
if let Err(message) = working_service.toggle_rule(params.rule_id, params.enabled) {
return Ok(IpcResponse::error(&message));
}
let next_rules = working_service.into_rules();
persist_rules_to_settings(state.inner(), &next_rules).await?;
replace_cached_rules(state.inner(), next_rules);
let rules = state.read().auto_score.read().get_rules().to_vec();
emit_auto_score_status_changed(&app_handle, &rules);
emit_rules_changed(&app_handle, state.inner());
Ok(IpcResponse::success(true))
}
#[tauri::command]
@@ -283,9 +303,8 @@ pub async fn auto_score_get_status(
}
}
let state_guard = state.read();
let auto_score_service = state_guard.auto_score.read();
let enabled = auto_score_service.is_enabled();
let rules = sync_cached_rules(state.inner()).await?;
let enabled = rules.iter().any(|rule| rule.enabled);
Ok(IpcResponse::success(AutoScoreStatus { enabled }))
}
@@ -304,27 +323,16 @@ pub async fn auto_score_sort_rules(
}
}
{
let state_guard = state.read();
let db_conn = state_guard.db.read().clone();
let mut auto_score_service = state_guard.auto_score.write();
let mut settings = state_guard.settings.write();
settings.attach_db(db_conn);
settings.initialize().await?;
let current_rules = sync_cached_rules(state.inner()).await?;
let mut working_service = AutoScoreService::from_rules(current_rules);
working_service.sort_rules(&rule_ids);
auto_score_service.sort_rules(&rule_ids);
let rules_json = auto_score_service.get_rules_json();
let _ = settings
.set_value(SettingsKey::AutoScoreRules, SettingsValue::Json(rules_json))
.await;
}
{
let state_guard = state.read();
let auto_score_service = state_guard.auto_score.read();
let _ = app_handle.emit("auto-score:rulesChanged", &auto_score_service.get_rules());
}
let next_rules = working_service.into_rules();
persist_rules_to_settings(state.inner(), &next_rules).await?;
replace_cached_rules(state.inner(), next_rules);
let rules = state.read().auto_score.read().get_rules().to_vec();
emit_auto_score_status_changed(&app_handle, &rules);
emit_rules_changed(&app_handle, state.inner());
Ok(IpcResponse::success(true))
}
+7
View File
@@ -142,6 +142,13 @@ pub async fn settings_set(
.map_err(|e| e.to_string())?;
}
if settings_key == SettingsKey::AutoScoreRules {
let state_guard = state.read();
let mut auto_score = state_guard.auto_score.write();
auto_score.load_rules(value.clone());
let _ = app_handle.emit("auto-score:rulesChanged", auto_score.get_rules());
}
let change = SettingChange {
key: key.clone(),
value,
+300 -91
View File
@@ -1,21 +1,24 @@
use chrono::{DateTime, Months, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use serde_json::Value as JsonValue;
use std::collections::HashSet;
use tauri::{AppHandle, Emitter};
#[derive(Debug, Clone, Serialize, Deserialize)]
const DEFAULT_INTERVAL_MINUTES: i64 = 30;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AutoScoreTrigger {
pub event: String,
pub value: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AutoScoreAction {
pub event: String,
pub value: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
enum IntervalUnit {
Minute,
@@ -23,13 +26,13 @@ enum IntervalUnit {
Month,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct IntervalTriggerValue {
amount: i64,
unit: IntervalUnit,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AutoScoreRule {
pub id: i32,
pub name: String,
@@ -75,6 +78,13 @@ impl AutoScoreService {
}
}
pub fn from_rules(rules: Vec<AutoScoreRule>) -> Self {
Self {
rules,
initialized: true,
}
}
pub async fn initialize(
&mut self,
_app_handle: &AppHandle,
@@ -86,96 +96,116 @@ impl AutoScoreService {
Ok(())
}
pub fn load_rules(&mut self, rules_json: serde_json::Value) {
if let serde_json::Value::Array(arr) = rules_json {
self.rules = arr
.into_iter()
.filter_map(|v| serde_json::from_value(v).ok())
.collect();
} else {
self.rules = Vec::new();
}
pub fn deserialize_rules(rules_json: &JsonValue) -> Vec<AutoScoreRule> {
let JsonValue::Array(items) = rules_json else {
return Vec::new();
};
items
.iter()
.filter_map(|item| serde_json::from_value::<AutoScoreRule>(item.clone()).ok())
.filter_map(|rule| normalize_rule(rule).ok())
.collect()
}
pub fn serialize_rules(rules: &[AutoScoreRule]) -> Result<JsonValue, String> {
serde_json::to_value(rules)
.map_err(|error| format!("Failed to serialize auto score rules: {}", error))
}
pub fn load_rules(&mut self, rules_json: JsonValue) {
self.rules = Self::deserialize_rules(&rules_json);
}
pub fn replace_rules(&mut self, rules: Vec<AutoScoreRule>) {
self.rules = rules;
}
pub fn into_rules(self) -> Vec<AutoScoreRule> {
self.rules
}
pub fn get_rules_json(&self) -> serde_json::Value {
serde_json::to_value(&self.rules).unwrap_or(serde_json::Value::Array(vec![]))
Self::serialize_rules(&self.rules).unwrap_or(JsonValue::Array(vec![]))
}
pub fn get_rules(&self) -> &[AutoScoreRule] {
&self.rules
}
pub fn get_rules_mut(&mut self) -> &mut Vec<AutoScoreRule> {
&mut self.rules
pub fn add_rule(&mut self, rule: AutoScoreRule) -> Result<i32, String> {
let mut normalized_rule = normalize_rule(rule)?;
let new_id = self.rules.iter().map(|item| item.id).max().unwrap_or(0) + 1;
normalized_rule.id = new_id;
normalized_rule.last_executed = None;
self.rules.push(normalized_rule);
Ok(new_id)
}
pub fn add_rule(&mut self, mut rule: AutoScoreRule) -> i32 {
let new_id = self.rules.iter().map(|r| r.id).max().unwrap_or(0) + 1;
rule.id = new_id;
rule.last_executed = None;
self.rules.push(rule);
new_id
pub fn update_rule(&mut self, rule: AutoScoreRule) -> Result<(), String> {
let Some(index) = self.rules.iter().position(|item| item.id == rule.id) else {
return Err("Rule not found".to_string());
};
let last_executed = self.rules[index].last_executed.clone();
let mut normalized_rule = normalize_rule(rule)?;
normalized_rule.id = self.rules[index].id;
normalized_rule.last_executed = normalize_last_executed(last_executed);
self.rules[index] = normalized_rule;
Ok(())
}
pub fn update_rule(&mut self, rule: AutoScoreRule) -> bool {
if let Some(existing) = self.rules.iter_mut().find(|r| r.id == rule.id) {
*existing = rule;
true
} else {
false
}
}
pub fn delete_rule(&mut self, rule_id: i32) -> bool {
pub fn delete_rule(&mut self, rule_id: i32) -> Result<(), String> {
let before_len = self.rules.len();
self.rules.retain(|r| r.id != rule_id);
self.rules.len() < before_len
}
pub fn toggle_rule(&mut self, rule_id: i32, enabled: bool) -> bool {
if let Some(rule) = self.rules.iter_mut().find(|r| r.id == rule_id) {
rule.enabled = enabled;
true
} else {
false
self.rules.retain(|rule| rule.id != rule_id);
if self.rules.len() == before_len {
return Err("Rule not found".to_string());
}
Ok(())
}
pub fn sort_rules(&mut self, rule_ids: &[i32]) -> bool {
let rule_map: HashMap<i32, AutoScoreRule> =
self.rules.drain(..).map(|r| (r.id, r)).collect();
pub fn toggle_rule(&mut self, rule_id: i32, enabled: bool) -> Result<(), String> {
let Some(rule) = self.rules.iter_mut().find(|item| item.id == rule_id) else {
return Err("Rule not found".to_string());
};
let mut sorted_rules: Vec<AutoScoreRule> = Vec::new();
for id in rule_ids {
if let Some(rule) = rule_map.get(id) {
sorted_rules.push(rule.clone());
rule.enabled = enabled;
Ok(())
}
pub fn sort_rules(&mut self, rule_ids: &[i32]) {
let mut ordered_rules = Vec::with_capacity(self.rules.len());
let mut consumed = HashSet::new();
for rule_id in rule_ids {
if !consumed.insert(*rule_id) {
continue;
}
if let Some(rule) = self.rules.iter().find(|item| item.id == *rule_id).cloned() {
ordered_rules.push(rule);
}
}
for (_, rule) in rule_map {
if !rule_ids.contains(&rule.id) {
sorted_rules.push(rule);
for rule in &self.rules {
if consumed.insert(rule.id) {
ordered_rules.push(rule.clone());
}
}
self.rules = sorted_rules;
true
self.rules = ordered_rules;
}
pub fn is_enabled(&self) -> bool {
self.rules.iter().any(|r| r.enabled)
self.rules.iter().any(|rule| rule.enabled)
}
pub fn get_rule_by_id(&self, id: i32) -> Option<&AutoScoreRule> {
self.rules.iter().find(|r| r.id == id)
}
pub fn get_rule_by_id_mut(&mut self, id: i32) -> Option<&mut AutoScoreRule> {
self.rules.iter_mut().find(|r| r.id == id)
self.rules.iter().find(|rule| rule.id == id)
}
pub fn mark_rule_executed(&mut self, rule_id: i32) {
if let Some(rule) = self.rules.iter_mut().find(|r| r.id == rule_id) {
if let Some(rule) = self.rules.iter_mut().find(|item| item.id == rule_id) {
rule.last_executed = Some(Utc::now().to_rfc3339());
}
}
@@ -184,20 +214,29 @@ impl AutoScoreService {
let now = Utc::now();
for trigger in &rule.triggers {
if trigger.event == "interval_time_passed" {
let interval = parse_interval_trigger_value(trigger.value.as_ref());
let base_time = rule
.last_executed
.as_ref()
.and_then(|value| DateTime::parse_from_rfc3339(value).ok())
.map(|value| value.with_timezone(&Utc))
.unwrap_or(now);
let next_execute_time = add_interval_to_time(base_time, &interval)?;
let delay_ms = (next_execute_time - now).num_milliseconds();
return Some(delay_ms.max(0));
if trigger.event != "interval_time_passed" {
continue;
}
let interval = parse_interval_trigger_value(trigger.value.as_deref()).unwrap_or(
IntervalTriggerValue {
amount: DEFAULT_INTERVAL_MINUTES,
unit: IntervalUnit::Minute,
},
);
let base_time = rule
.last_executed
.as_ref()
.and_then(|value| DateTime::parse_from_rfc3339(value).ok())
.map(|value| value.with_timezone(&Utc))
.unwrap_or(now);
let next_execute_time = add_interval_to_time(base_time, &interval)?;
let delay_ms = (next_execute_time - now).num_milliseconds();
return Some(delay_ms.max(0));
}
None
}
@@ -206,27 +245,197 @@ impl AutoScoreService {
}
}
fn parse_interval_trigger_value(value: Option<&String>) -> IntervalTriggerValue {
if let Some(raw_value) = value {
if let Ok(minutes) = raw_value.parse::<i64>() {
if minutes > 0 {
return IntervalTriggerValue {
amount: minutes,
unit: IntervalUnit::Minute,
};
}
fn normalize_rule(mut rule: AutoScoreRule) -> Result<AutoScoreRule, String> {
rule.name = normalize_required_string(rule.name, "automation name")?;
rule.student_names = dedupe_trimmed_strings(rule.student_names);
if rule.triggers.is_empty() {
return Err("At least one trigger is required".to_string());
}
if rule.actions.is_empty() {
return Err("At least one action is required".to_string());
}
rule.triggers = rule
.triggers
.into_iter()
.map(normalize_trigger)
.collect::<Result<Vec<_>, _>>()?;
rule.actions = rule
.actions
.into_iter()
.map(normalize_action)
.collect::<Result<Vec<_>, _>>()?;
rule.last_executed = normalize_last_executed(rule.last_executed);
Ok(rule)
}
fn normalize_trigger(trigger: AutoScoreTrigger) -> Result<AutoScoreTrigger, String> {
let event = normalize_required_string(trigger.event, "trigger event")?;
match event.as_str() {
"interval_time_passed" => {
let interval = parse_interval_trigger_value(trigger.value.as_deref())
.ok_or_else(|| "Invalid interval trigger value".to_string())?;
let value = stringify_interval_trigger_value(&interval)
.ok_or_else(|| "Invalid interval trigger value".to_string())?;
Ok(AutoScoreTrigger {
event,
value: Some(value),
})
}
"student_has_tag" => {
let tag_values = parse_tag_values(trigger.value.as_deref());
let value = stringify_tag_values(&tag_values)
.ok_or_else(|| "Student tag trigger requires at least one tag".to_string())?;
Ok(AutoScoreTrigger {
event,
value: Some(value),
})
}
_ => Err(format!("Unsupported trigger event: {}", event)),
}
}
fn normalize_action(action: AutoScoreAction) -> Result<AutoScoreAction, String> {
let event = normalize_required_string(action.event, "action event")?;
match event.as_str() {
"add_score" => {
let value = normalize_non_zero_numeric_string(action.value.as_deref())
.ok_or_else(|| "Add score action requires a non-zero numeric value".to_string())?;
Ok(AutoScoreAction {
event,
value: Some(value),
})
}
"add_tag" => {
let tag_values = parse_tag_values(action.value.as_deref());
let value = stringify_tag_values(&tag_values)
.ok_or_else(|| "Add tag action requires at least one tag".to_string())?;
Ok(AutoScoreAction {
event,
value: Some(value),
})
}
_ => Err(format!("Unsupported action event: {}", event)),
}
}
fn normalize_required_string(value: String, field_name: &str) -> Result<String, String> {
let normalized = value.trim().to_string();
if normalized.is_empty() {
return Err(format!("{} is required", field_name));
}
Ok(normalized)
}
fn dedupe_trimmed_strings(values: Vec<String>) -> Vec<String> {
let mut seen = HashSet::new();
let mut normalized = Vec::new();
for value in values {
let trimmed = value.trim();
if trimmed.is_empty() {
continue;
}
if let Ok(parsed_value) = serde_json::from_str::<IntervalTriggerValue>(raw_value) {
if parsed_value.amount > 0 {
return parsed_value;
}
let owned = trimmed.to_string();
if seen.insert(owned.clone()) {
normalized.push(owned);
}
}
IntervalTriggerValue {
amount: 30,
unit: IntervalUnit::Minute,
normalized
}
fn parse_tag_values(value: Option<&str>) -> Vec<String> {
let Some(raw_value) = value.map(str::trim) else {
return Vec::new();
};
if raw_value.is_empty() {
return Vec::new();
}
if raw_value.starts_with('[') {
if let Ok(parsed_values) = serde_json::from_str::<Vec<String>>(raw_value) {
return dedupe_trimmed_strings(parsed_values);
}
}
dedupe_trimmed_strings(vec![raw_value.to_string()])
}
fn stringify_tag_values(values: &[String]) -> Option<String> {
if values.is_empty() {
return None;
}
if values.len() == 1 {
return values.first().cloned();
}
serde_json::to_string(values).ok()
}
fn normalize_non_zero_numeric_string(value: Option<&str>) -> Option<String> {
let normalized = value?.trim();
if normalized.is_empty() {
return None;
}
let parsed = normalized.parse::<f64>().ok()?;
if !parsed.is_finite() || parsed == 0.0 {
return None;
}
Some(normalized.to_string())
}
fn normalize_last_executed(value: Option<String>) -> Option<String> {
value
.as_deref()
.and_then(|raw_value| DateTime::parse_from_rfc3339(raw_value).ok())
.map(|parsed| parsed.with_timezone(&Utc).to_rfc3339())
}
fn parse_interval_trigger_value(value: Option<&str>) -> Option<IntervalTriggerValue> {
let raw_value = value.map(str::trim)?;
if raw_value.is_empty() {
return None;
}
if let Ok(minutes) = raw_value.parse::<i64>() {
if minutes > 0 {
return Some(IntervalTriggerValue {
amount: minutes,
unit: IntervalUnit::Minute,
});
}
}
let parsed_value = serde_json::from_str::<IntervalTriggerValue>(raw_value).ok()?;
if parsed_value.amount <= 0 {
return None;
}
Some(parsed_value)
}
fn stringify_interval_trigger_value(value: &IntervalTriggerValue) -> Option<String> {
if value.amount <= 0 {
return None;
}
match value.unit {
IntervalUnit::Minute => Some(value.amount.to_string()),
IntervalUnit::Day | IntervalUnit::Month => serde_json::to_string(value).ok(),
}
}
+13 -1
View File
@@ -67,7 +67,19 @@ impl AppState {
theme.load_saved_theme(&current_theme_id);
}
self.auto_score.write().initialize(&self.app_handle).await?;
let auto_score_rules = {
let settings = self.settings.read();
match settings.get_value(SettingsKey::AutoScoreRules) {
SettingsValue::Json(value) => value,
_ => serde_json::Value::Array(vec![]),
}
};
{
let mut auto_score = self.auto_score.write();
auto_score.load_rules(auto_score_rules);
auto_score.initialize(&self.app_handle).await?;
}
Ok(())
}
}