feat(xai): expose CLI release check status

Record successful and failed official npm checks, retain the last successful version on failure, and publish the checked timestamp through the dashboard wire profile.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
zyycn
2026-07-26 03:24:04 +08:00
co-authored by Codex
parent cbe8699322
commit fee34b6bd1
4 changed files with 218 additions and 17 deletions
+54 -11
View File
@@ -11,8 +11,9 @@ use gateway_admin::model::accounts::{
AccountAvailability as AdminAccountAvailability, AccountRecord,
};
use gateway_admin::model::observability::{
CalculatedBillingBreakdown, CurrencyCost, DashboardWireAttribute, DashboardWireProfile,
DashboardWireTarget, DecimalAmount, ProviderBillingInput,
CalculatedBillingBreakdown, CurrencyCost, DashboardDesktopRelease, DashboardWireAttribute,
DashboardWireProfile, DashboardWireTarget, DecimalAmount, DesktopReleaseStatus,
ProviderBillingInput,
};
use gateway_admin::model::provider_credentials::{
AuthorizationMutationTarget, AuthorizationOwner, AuthorizationOwnerBinding,
@@ -57,6 +58,7 @@ use crate::credential::{
PreparedGrokCredentialRotation, PreparedGrokCredentialRotationGuard, RedirectUriAllowlist,
RotateManagedGrokCredential, SecretValue, VerifiedGrokAccount, VerifiedTokenSet,
};
use crate::transport::profile::{GrokCliReleaseSnapshot, GrokCliReleaseStatus};
use crate::transport::{GROK_CLI_BASE_URL, XAI_PROVIDER_NAME, grok_billing_breakdown};
const PENDING_SCHEMA_VERSION: u64 = 2;
@@ -66,6 +68,7 @@ const MAX_PENDING_TEXT_BYTES: usize = 512;
pub(crate) struct XaiAdminProvider {
provider_kind: ProviderKind,
wire_profile: XaiWireProfileState,
cli_release: GrokCliReleaseStatus,
accounts: Arc<dyn ProviderAccountStore>,
repository: GrokCredentialRepository,
oauth_config: GrokOAuthConfig,
@@ -95,10 +98,12 @@ impl XaiAdminProvider {
wire_profile: XaiWireProfileState,
accounts: Arc<dyn ProviderAccountStore>,
services: XaiAdminServices,
cli_release: GrokCliReleaseStatus,
) -> Self {
Self {
provider_kind,
wire_profile,
cli_release,
accounts,
repository: services.repository,
oauth_config: services.oauth_config,
@@ -216,34 +221,42 @@ impl ProviderAdmin for XaiAdminProvider {
}
fn dashboard_wire_profile(&self) -> Option<DashboardWireProfile> {
let profile = self.wire_profile.snapshot();
let release = dashboard_cli_release(&profile.client_version, self.cli_release.snapshot());
Some(DashboardWireProfile {
provider: self.provider_kind.as_str().to_owned(),
product: "Grok Build".to_owned(),
version: self.wire_profile.client_version(),
version: profile.client_version.clone(),
build: None,
target: DashboardWireTarget {
os_type: self.wire_profile.target_os(),
os_type: profile.target_os.clone(),
os_version: "—".to_owned(),
arch: self.wire_profile.target_arch(),
terminal: self.wire_profile.client_mode(),
arch: profile.target_arch.clone(),
terminal: profile.client_mode.clone(),
},
user_agent: self.wire_profile.user_agent(),
user_agent: format!(
"{}/{} ({}; {})",
profile.client_identifier,
profile.client_version,
profile.target_os,
profile.target_arch
),
attributes: vec![
DashboardWireAttribute {
label: "客户端标识".to_owned(),
value: self.wire_profile.client_identifier(),
value: profile.client_identifier,
},
DashboardWireAttribute {
label: "运行模式".to_owned(),
value: self.wire_profile.client_mode(),
value: profile.client_mode,
},
DashboardWireAttribute {
label: "Token 认证".to_owned(),
value: "xai-grok-cli".to_owned(),
},
],
verified_at: Some(self.wire_profile.verified_at()),
release: None,
verified_at: Some(profile.verified_at),
release: Some(release),
})
}
@@ -771,6 +784,36 @@ fn validate_account_record(
Ok(())
}
fn dashboard_cli_release(
profile_version: &str,
snapshot: GrokCliReleaseSnapshot,
) -> DashboardDesktopRelease {
let status = if snapshot.checked_at.is_none() {
DesktopReleaseStatus::Unchecked
} else if snapshot.last_error.is_some() {
DesktopReleaseStatus::Failed
} else if snapshot.latest_version.as_deref() == Some(profile_version) {
DesktopReleaseStatus::Current
} else if snapshot.latest_version.is_some() {
DesktopReleaseStatus::UpdateAvailable
} else {
DesktopReleaseStatus::Failed
};
DashboardDesktopRelease {
status,
checked_at: snapshot.checked_at,
latest_version: snapshot.latest_version,
latest_build: None,
published_at: None,
minimum_system_version: None,
hardware_requirements: None,
download_url: None,
download_size: None,
signature_present: None,
error: snapshot.last_error,
}
}
fn account_from_record(account: &AccountRecord) -> Result<ProviderAccount, ProviderAdminError> {
let account_id = ProviderAccountId::new(account.id.clone())
.map_err(|_| provider_error(ProviderAdminErrorKind::Invalid))?;
+2
View File
@@ -100,6 +100,7 @@ pub async fn initialize(
OfficialGrokCliReleaseTransport::new().map_err(|_| XaiInitializeError::Transport)?,
),
));
let cli_release_status = cli_release.status();
let provider_kind =
ProviderKind::new(XAI_PROVIDER_NAME).map_err(|_| XaiInitializeError::ProviderKind)?;
let accounts: Arc<dyn ProviderAccountStore> = ports.accounts();
@@ -194,6 +195,7 @@ pub async fn initialize(
catalog: Arc::clone(&catalog),
runtime_policy,
},
cli_release_status,
));
let worker_contributions = provider::worker_contributions(
refresh,
@@ -145,18 +145,82 @@ impl GrokCliReleaseTransport for OfficialGrokCliReleaseTransport {
pub struct GrokCliReleaseService {
profile: XaiWireProfileState,
transport: Arc<dyn GrokCliReleaseTransport>,
status: GrokCliReleaseStatus,
}
impl GrokCliReleaseService {
#[must_use]
pub fn new(profile: XaiWireProfileState, transport: Arc<dyn GrokCliReleaseTransport>) -> Self {
Self { profile, transport }
Self {
profile,
transport,
status: GrokCliReleaseStatus::default(),
}
}
#[must_use]
pub(crate) fn status(&self) -> GrokCliReleaseStatus {
self.status.clone()
}
pub async fn refresh(&self) -> Result<String, GrokCliReleaseError> {
let version = self.transport.fetch().await?;
self.profile.update_client_version(&version);
Ok(version)
let checked_at = Utc::now();
let result = self.transport.fetch().await;
match result {
Ok(version) => {
self.profile.update_client_version(&version);
self.status.record_success(checked_at, version.clone());
Ok(version)
}
Err(error) => {
self.status.record_failure(checked_at, &error);
Err(error)
}
}
}
}
/// 最近一次官方 Grok CLI 发布检查结果。
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct GrokCliReleaseSnapshot {
pub(crate) checked_at: Option<DateTime<Utc>>,
pub(crate) latest_version: Option<String>,
pub(crate) last_error: Option<String>,
}
/// Provider 内共享的 Grok CLI 发布检查观察状态。
#[derive(Debug, Clone, Default)]
pub(crate) struct GrokCliReleaseStatus {
snapshot: Arc<RwLock<GrokCliReleaseSnapshot>>,
}
impl GrokCliReleaseStatus {
#[must_use]
pub(crate) fn snapshot(&self) -> GrokCliReleaseSnapshot {
self.snapshot
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
fn record_success(&self, checked_at: DateTime<Utc>, latest_version: String) {
*self
.snapshot
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = GrokCliReleaseSnapshot {
checked_at: Some(checked_at),
latest_version: Some(latest_version),
last_error: None,
};
}
fn record_failure(&self, checked_at: DateTime<Utc>, error: &GrokCliReleaseError) {
let mut snapshot = self
.snapshot
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
snapshot.checked_at = Some(checked_at);
snapshot.last_error = Some(error.to_string());
}
}
@@ -191,3 +255,91 @@ pub enum GrokCliReleaseError {
#[error("Grok CLI release version is invalid")]
InvalidVersion,
}
#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use std::sync::Mutex;
use super::*;
struct ReleaseTransport {
outcomes: Mutex<VecDeque<Result<String, GrokCliReleaseError>>>,
}
impl ReleaseTransport {
fn new(outcomes: impl IntoIterator<Item = Result<String, GrokCliReleaseError>>) -> Self {
Self {
outcomes: Mutex::new(outcomes.into_iter().collect()),
}
}
}
impl GrokCliReleaseTransport for ReleaseTransport {
fn fetch(&self) -> BoxFuture<'_, Result<String, GrokCliReleaseError>> {
let outcome = self
.outcomes
.lock()
.expect("release outcomes lock")
.pop_front()
.expect("release outcome");
Box::pin(async move { outcome })
}
}
#[tokio::test]
async fn release_refresh_should_update_profile_and_observation() {
let profile = wire_profile();
let service = GrokCliReleaseService::new(
profile.clone(),
Arc::new(ReleaseTransport::new([Ok("0.2.112".to_owned())])),
);
let status = service.status();
assert_eq!(service.refresh().await.expect("release"), "0.2.112");
assert_eq!(profile.client_version(), "0.2.112");
let snapshot = status.snapshot();
assert!(snapshot.checked_at.is_some());
assert_eq!(snapshot.latest_version.as_deref(), Some("0.2.112"));
assert!(snapshot.last_error.is_none());
}
#[tokio::test]
async fn failed_release_refresh_should_keep_last_successful_version() {
let profile = wire_profile();
let service = GrokCliReleaseService::new(
profile.clone(),
Arc::new(ReleaseTransport::new([
Ok("0.2.112".to_owned()),
Err(GrokCliReleaseError::InvalidDocument),
])),
);
let status = service.status();
service.refresh().await.expect("first release");
assert!(matches!(
service.refresh().await,
Err(GrokCliReleaseError::InvalidDocument)
));
assert_eq!(profile.client_version(), "0.2.112");
let snapshot = status.snapshot();
assert!(snapshot.checked_at.is_some());
assert_eq!(snapshot.latest_version.as_deref(), Some("0.2.112"));
assert_eq!(
snapshot.last_error.as_deref(),
Some("Grok CLI release document is invalid")
);
}
fn wire_profile() -> XaiWireProfileState {
XaiWireProfileState::new(XaiWireProfileConfig {
client_identifier: "grok-shell".to_owned(),
client_version: "0.2.106".to_owned(),
client_mode: "headless".to_owned(),
target_os: "linux".to_owned(),
target_arch: "x86_64".to_owned(),
verified_at: Utc::now(),
})
}
}
+6 -2
View File
@@ -9,7 +9,9 @@ use futures::{StreamExt as _, future::BoxFuture};
use gateway_admin::model::accounts::{
AccountAvailability as AdminAccountAvailability, AccountRecord,
};
use gateway_admin::model::observability::{CurrencyCost, ProviderBillingInput};
use gateway_admin::model::observability::{
CurrencyCost, DesktopReleaseStatus, ProviderBillingInput,
};
use gateway_admin::model::provider_credentials::{
CompleteAuthorization, PrepareCredentialImport, PrepareCredentialRefresh,
PrepareCredentialRotation, ProviderDocument, ProviderExportCredentialInput,
@@ -281,7 +283,9 @@ async fn xai_admin_provider_validates_known_billing_breakdown() {
assert_eq!(profile.product, "Grok Build");
assert_eq!(profile.version, "0.2.106");
assert_eq!(profile.user_agent, "grok-shell/0.2.106 (linux; x86_64)");
assert!(profile.release.is_none());
let release = profile.release.expect("release status");
assert_eq!(release.status, DesktopReleaseStatus::Unchecked);
assert!(release.checked_at.is_none());
let billing = admin
.calculated_billing(&ProviderBillingInput {