use std::time::Duration;
use atomcode_core::config::Config;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CodingPlanWarning {
ModelMissing(String),
StaleList,
}
impl CodingPlanWarning {
pub fn display_text(&self) -> String {
match self {
Self::ModelMissing(name) => format!("⚠ '{}' 已下线 — /codingplan", name),
Self::StaleList => "ⓘ CodingPlan 模型列表更新 — 可执行/codingplan".into(),
}
}
}
pub const CHECK_COOLDOWN: Duration = Duration::from_secs(3600);
const PROVIDER_PREFIX: &str = "AtomGit";
pub fn is_codingplan_provider(name: &str) -> bool {
name == PROVIDER_PREFIX || name.starts_with(&format!("{}-", PROVIDER_PREFIX))
}
pub fn local_atomgit_models(config: &Config) -> Vec<String> {
config
.providers
.iter()
.filter(|(k, _)| is_codingplan_provider(k))
.map(|(_, p)| p.model.clone())
.collect()
}
pub fn decide_warning(
default_model: &str,
server_models: &[String],
local_models: &[String],
) -> Option<CodingPlanWarning> {
if !server_models.iter().any(|m| m == default_model) {
return Some(CodingPlanWarning::ModelMissing(default_model.to_string()));
}
if !sorted_eq(server_models, local_models) {
return Some(CodingPlanWarning::StaleList);
}
None
}
pub fn spawn_check(
config_snapshot: atomcode_core::config::Config,
default_model: String,
slot: std::sync::Arc<std::sync::Mutex<Option<CodingPlanWarning>>>,
wake_tx: tokio::sync::mpsc::Sender<()>,
) {
tokio::spawn(async move {
let fetch: Result<Vec<String>, ()> = tokio::task::spawn_blocking(move || {
let client =
atomcode_core::coding_plan::client::Client::from_stored_auth().map_err(|_| ())?;
let models = client
.list_models_v2(atomcode_core::coding_plan::PlanType::Max)
.map_err(|_| ())?;
Ok(models
.into_iter()
.filter(|m| m.plan_available)
.map(|m| m.display_model_name)
.collect())
})
.await
.unwrap_or(Err(()));
let server_models = match fetch {
Ok(v) => v,
Err(_) => return, };
let local_models = local_atomgit_models(&config_snapshot);
let warning = decide_warning(&default_model, &server_models, &local_models);
if let Ok(mut g) = slot.lock() {
*g = warning;
}
let _ = wake_tx.try_send(());
});
}
fn sorted_eq(a: &[String], b: &[String]) -> bool {
if a.len() != b.len() {
return false;
}
let mut a: Vec<&str> = a.iter().map(|s| s.as_str()).collect();
let mut b: Vec<&str> = b.iter().map(|s| s.as_str()).collect();
a.sort_unstable();
b.sort_unstable();
a == b
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn is_codingplan_provider_matches_prefix_and_exact() {
assert!(is_codingplan_provider("AtomGit"));
assert!(is_codingplan_provider("AtomGit-moonshotai-Kimi"));
assert!(!is_codingplan_provider("AtomGitPlus"));
assert!(!is_codingplan_provider("atomgit"));
assert!(!is_codingplan_provider("claude"));
}
#[test]
fn sorted_eq_ignores_order() {
assert!(sorted_eq(&s(&["a", "b"]), &s(&["b", "a"])));
assert!(!sorted_eq(&s(&["a", "b"]), &s(&["a", "c"])));
assert!(!sorted_eq(&s(&["a", "b"]), &s(&["a"])));
assert!(sorted_eq(&s(&[]), &s(&[])));
}
#[test]
fn decide_no_warning_when_in_list_and_match() {
let server = s(&["m1", "m2"]);
let local = s(&["m1", "m2"]);
assert_eq!(decide_warning("m1", &server, &local), None);
}
#[test]
fn decide_no_warning_when_lists_match_out_of_order() {
let server = s(&["m1", "m2"]);
let local = s(&["m2", "m1"]);
assert_eq!(decide_warning("m1", &server, &local), None);
}
#[test]
fn decide_stale_warning_whenever_lists_differ() {
let server = s(&["m1", "m2"]);
let local = s(&["m1"]); assert_eq!(
decide_warning("m1", &server, &local),
Some(CodingPlanWarning::StaleList)
);
}
#[test]
fn decide_model_missing_when_active_model_gone() {
let server = s(&["m2", "m3"]);
let local = s(&["m1", "m2", "m3"]);
assert_eq!(
decide_warning("m1", &server, &local),
Some(CodingPlanWarning::ModelMissing("m1".into()))
);
}
#[test]
fn decide_model_missing_wins_over_stale() {
let server = s(&["m2"]);
let local = s(&["m1"]);
assert_eq!(
decide_warning("m1", &server, &local),
Some(CodingPlanWarning::ModelMissing("m1".into()))
);
}
#[test]
fn display_text_format() {
assert_eq!(
CodingPlanWarning::ModelMissing("Kimi-K2".into()).display_text(),
"⚠ 'Kimi-K2' 已下线 — /codingplan"
);
assert_eq!(
CodingPlanWarning::StaleList.display_text(),
"ⓘ CodingPlan 模型列表更新 — 可执行/codingplan"
);
}
}