mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
Merge branch 'pr-438' into aether-rust-pioneer
This commit is contained in:
@@ -13,8 +13,8 @@ use crate::handlers::admin::system::shared::paths::{
|
|||||||
};
|
};
|
||||||
use crate::handlers::admin::system::shared::settings::{
|
use crate::handlers::admin::system::shared::settings::{
|
||||||
apply_admin_system_settings_update, build_admin_api_formats_payload,
|
apply_admin_system_settings_update, build_admin_api_formats_payload,
|
||||||
build_admin_system_check_update_payload, build_admin_system_settings_payload,
|
build_admin_system_check_update_payload_from_release, build_admin_system_settings_payload,
|
||||||
build_admin_system_stats_payload, current_aether_version,
|
build_admin_system_stats_payload, current_aether_version, fetch_latest_admin_system_release,
|
||||||
};
|
};
|
||||||
use crate::handlers::admin::system::shared::smtp::build_admin_smtp_test_payload;
|
use crate::handlers::admin::system::shared::smtp::build_admin_smtp_test_payload;
|
||||||
use crate::GatewayError;
|
use crate::GatewayError;
|
||||||
@@ -54,8 +54,13 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
|||||||
&& request_method == http::Method::GET
|
&& request_method == http::Method::GET
|
||||||
&& request_path == "/api/admin/system/check-update"
|
&& request_path == "/api/admin/system/check-update"
|
||||||
{
|
{
|
||||||
|
let (latest_release, error) = fetch_latest_admin_system_release().await;
|
||||||
return Ok(Some(
|
return Ok(Some(
|
||||||
Json(build_admin_system_check_update_payload()).into_response(),
|
Json(build_admin_system_check_update_payload_from_release(
|
||||||
|
latest_release,
|
||||||
|
error,
|
||||||
|
))
|
||||||
|
.into_response(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,22 @@ use crate::GatewayError;
|
|||||||
use aether_admin::system::{
|
use aether_admin::system::{
|
||||||
build_admin_api_formats_payload as build_admin_api_formats_payload_pure,
|
build_admin_api_formats_payload as build_admin_api_formats_payload_pure,
|
||||||
build_admin_system_check_update_payload as build_admin_system_check_update_payload_pure,
|
build_admin_system_check_update_payload as build_admin_system_check_update_payload_pure,
|
||||||
|
build_admin_system_check_update_payload_with_release,
|
||||||
build_admin_system_settings_payload as build_admin_system_settings_payload_pure,
|
build_admin_system_settings_payload as build_admin_system_settings_payload_pure,
|
||||||
build_admin_system_settings_updated_payload,
|
build_admin_system_settings_updated_payload,
|
||||||
build_admin_system_stats_payload as build_admin_system_stats_payload_pure,
|
build_admin_system_stats_payload as build_admin_system_stats_payload_pure,
|
||||||
parse_admin_system_settings_update,
|
parse_admin_system_settings_update, AdminSystemUpdateRelease,
|
||||||
};
|
};
|
||||||
use axum::body::Bytes;
|
use axum::body::Bytes;
|
||||||
use axum::http;
|
use axum::http;
|
||||||
|
#[cfg(not(test))]
|
||||||
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
#[cfg(not(test))]
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const AETHER_RELEASES_API_URL: &str =
|
||||||
|
"https://api.github.com/repos/fawney19/Aether/releases?per_page=20";
|
||||||
|
|
||||||
pub(crate) fn current_aether_version() -> String {
|
pub(crate) fn current_aether_version() -> String {
|
||||||
option_env!("AETHER_BUILD_VERSION")
|
option_env!("AETHER_BUILD_VERSION")
|
||||||
@@ -24,6 +32,75 @@ pub(crate) fn build_admin_system_check_update_payload() -> serde_json::Value {
|
|||||||
build_admin_system_check_update_payload_pure(current_aether_version())
|
build_admin_system_check_update_payload_pure(current_aether_version())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_admin_system_check_update_payload_from_release(
|
||||||
|
latest_release: Option<AdminSystemUpdateRelease>,
|
||||||
|
error: Option<String>,
|
||||||
|
) -> serde_json::Value {
|
||||||
|
build_admin_system_check_update_payload_with_release(
|
||||||
|
current_aether_version(),
|
||||||
|
latest_release,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
pub(crate) async fn fetch_latest_admin_system_release(
|
||||||
|
) -> (Option<AdminSystemUpdateRelease>, Option<String>) {
|
||||||
|
match fetch_latest_admin_system_release_inner().await {
|
||||||
|
Ok(release) => (release, None),
|
||||||
|
Err(err) => (None, Some(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) async fn fetch_latest_admin_system_release(
|
||||||
|
) -> (Option<AdminSystemUpdateRelease>, Option<String>) {
|
||||||
|
(None, Some("测试环境未请求 GitHub Releases".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
async fn fetch_latest_admin_system_release_inner(
|
||||||
|
) -> Result<Option<AdminSystemUpdateRelease>, String> {
|
||||||
|
let releases: Vec<GitHubRelease> = reqwest::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(8))
|
||||||
|
.build()
|
||||||
|
.map_err(|err| format!("创建更新检查客户端失败: {err}"))?
|
||||||
|
.get(AETHER_RELEASES_API_URL)
|
||||||
|
.header(reqwest::header::USER_AGENT, "Aether-Gateway update-check")
|
||||||
|
.header(reqwest::header::ACCEPT, "application/vnd.github+json")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("请求 GitHub Releases 失败: {err}"))?
|
||||||
|
.error_for_status()
|
||||||
|
.map_err(|err| format!("GitHub Releases 返回错误: {err}"))?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("解析 GitHub Releases 失败: {err}"))?;
|
||||||
|
|
||||||
|
Ok(releases
|
||||||
|
.into_iter()
|
||||||
|
.find(|release| !release.draft && release.tag_name.starts_with('v'))
|
||||||
|
.map(|release| AdminSystemUpdateRelease {
|
||||||
|
version: release.tag_name,
|
||||||
|
release_url: Some(release.html_url),
|
||||||
|
release_notes: release.body.filter(|body| !body.trim().is_empty()),
|
||||||
|
published_at: release.published_at,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct GitHubRelease {
|
||||||
|
tag_name: String,
|
||||||
|
html_url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
body: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
published_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
draft: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn build_admin_system_stats_payload(
|
pub(crate) async fn build_admin_system_stats_payload(
|
||||||
state: &AdminAppState<'_>,
|
state: &AdminAppState<'_>,
|
||||||
) -> Result<serde_json::Value, GatewayError> {
|
) -> Result<serde_json::Value, GatewayError> {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ async fn gateway_handles_admin_system_check_update_locally_with_trusted_admin_pr
|
|||||||
.is_some_and(|value| !value.is_empty()));
|
.is_some_and(|value| !value.is_empty()));
|
||||||
assert_eq!(payload["latest_version"], serde_json::Value::Null);
|
assert_eq!(payload["latest_version"], serde_json::Value::Null);
|
||||||
assert_eq!(payload["has_update"], json!(false));
|
assert_eq!(payload["has_update"], json!(false));
|
||||||
assert_eq!(payload["error"], "检查更新需要 Rust 管理后端");
|
assert_eq!(payload["error"], "测试环境未请求 GitHub Releases");
|
||||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||||
|
|
||||||
gateway_handle.abort();
|
gateway_handle.abort();
|
||||||
@@ -151,7 +151,7 @@ async fn gateway_handles_admin_system_check_update_locally_with_bearer_admin_ses
|
|||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||||
assert_eq!(payload["has_update"], json!(false));
|
assert_eq!(payload["has_update"], json!(false));
|
||||||
assert_eq!(payload["error"], "检查更新需要 Rust 管理后端");
|
assert_eq!(payload["error"], "测试环境未请求 GitHub Releases");
|
||||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||||
|
|
||||||
gateway_handle.abort();
|
gateway_handle.abort();
|
||||||
|
|||||||
@@ -58,6 +58,14 @@ pub const ADMIN_SYSTEM_PROVIDER_OPS_SENSITIVE_CREDENTIAL_FIELDS: &[&str] = &[
|
|||||||
"cookie",
|
"cookie",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct AdminSystemUpdateRelease {
|
||||||
|
pub version: String,
|
||||||
|
pub release_url: Option<String>,
|
||||||
|
pub release_notes: Option<String>,
|
||||||
|
pub published_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
fn default_true() -> bool {
|
fn default_true() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -620,17 +628,42 @@ const ADMIN_API_FORMAT_DEFINITIONS: &[AdminApiFormatDefinition] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
pub fn build_admin_system_check_update_payload(current_version: String) -> serde_json::Value {
|
pub fn build_admin_system_check_update_payload(current_version: String) -> serde_json::Value {
|
||||||
|
build_admin_system_check_update_payload_with_release(
|
||||||
|
current_version,
|
||||||
|
None,
|
||||||
|
Some("检查更新需要 Rust 管理后端".to_string()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_admin_system_check_update_payload_with_release(
|
||||||
|
current_version: String,
|
||||||
|
latest_release: Option<AdminSystemUpdateRelease>,
|
||||||
|
error: Option<String>,
|
||||||
|
) -> serde_json::Value {
|
||||||
|
let has_update = latest_release.as_ref().is_some_and(|release| {
|
||||||
|
normalized_admin_system_version(&release.version)
|
||||||
|
!= normalized_admin_system_version(¤t_version)
|
||||||
|
});
|
||||||
|
|
||||||
json!({
|
json!({
|
||||||
"current_version": current_version,
|
"current_version": current_version,
|
||||||
"latest_version": serde_json::Value::Null,
|
"latest_version": latest_release.as_ref().map(|release| release.version.clone()),
|
||||||
"has_update": false,
|
"has_update": has_update,
|
||||||
"release_url": serde_json::Value::Null,
|
"release_url": latest_release.as_ref().and_then(|release| release.release_url.clone()),
|
||||||
"release_notes": serde_json::Value::Null,
|
"release_notes": latest_release.as_ref().and_then(|release| release.release_notes.clone()),
|
||||||
"published_at": serde_json::Value::Null,
|
"published_at": latest_release.as_ref().and_then(|release| release.published_at.clone()),
|
||||||
"error": "检查更新需要 Rust 管理后端",
|
"error": error,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalized_admin_system_version(version: &str) -> String {
|
||||||
|
version
|
||||||
|
.trim()
|
||||||
|
.strip_prefix('v')
|
||||||
|
.unwrap_or(version.trim())
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_admin_system_stats_payload(
|
pub fn build_admin_system_stats_payload(
|
||||||
total_users: u64,
|
total_users: u64,
|
||||||
active_users: u64,
|
active_users: u64,
|
||||||
@@ -2180,6 +2213,50 @@ fn mask_admin_proxy_node_password(password: Option<&str>) -> Option<String> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_admin_system_check_update_payload_reports_available_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: Some(
|
||||||
|
"https://github.com/fawney19/Aether/releases/tag/v0.7.0-rc28".to_string(),
|
||||||
|
),
|
||||||
|
release_notes: Some("release notes".to_string()),
|
||||||
|
published_at: Some("2026-05-13T00:00:00Z".to_string()),
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(payload["current_version"], "0.7.0-rc27");
|
||||||
|
assert_eq!(payload["latest_version"], "v0.7.0-rc28");
|
||||||
|
assert_eq!(payload["has_update"], true);
|
||||||
|
assert_eq!(
|
||||||
|
payload["release_url"],
|
||||||
|
"https://github.com/fawney19/Aether/releases/tag/v0.7.0-rc28"
|
||||||
|
);
|
||||||
|
assert_eq!(payload["release_notes"], "release notes");
|
||||||
|
assert_eq!(payload["published_at"], "2026-05-13T00:00:00Z");
|
||||||
|
assert_eq!(payload["error"], 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(
|
||||||
|
"0.7.0-rc28".to_string(),
|
||||||
|
Some(AdminSystemUpdateRelease {
|
||||||
|
version: "v0.7.0-rc28".to_string(),
|
||||||
|
release_url: None,
|
||||||
|
release_notes: None,
|
||||||
|
published_at: None,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(payload["has_update"], false);
|
||||||
|
assert_eq!(payload["error"], serde_json::Value::Null);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_admin_system_config_import_request_accepts_supported_versions() {
|
fn parse_admin_system_config_import_request_accepts_supported_versions() {
|
||||||
let parsed = parse_admin_system_config_import_request(
|
let parsed = parse_admin_system_config_import_request(
|
||||||
|
|||||||
@@ -122,6 +122,77 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 管理员:版本与更新状态 -->
|
||||||
|
<Card
|
||||||
|
v-if="isAdmin"
|
||||||
|
class="relative mt-6 overflow-hidden p-4 sm:p-5 border-book-cloth/30"
|
||||||
|
>
|
||||||
|
<div class="pointer-events-none absolute -right-10 -top-10 h-32 w-32 rounded-full bg-book-cloth/20 blur-3xl" />
|
||||||
|
<div class="relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="mb-2 flex items-center gap-2">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
class="uppercase tracking-[0.3em] text-[10px]"
|
||||||
|
>
|
||||||
|
Version
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-if="updateStatus"
|
||||||
|
:variant="updateStatus.has_update ? 'default' : 'secondary'"
|
||||||
|
class="text-[10px]"
|
||||||
|
>
|
||||||
|
{{ updateStatusLabel }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-sm font-medium text-foreground">
|
||||||
|
系统版本
|
||||||
|
</h3>
|
||||||
|
<p class="mt-2 text-sm text-muted-foreground">
|
||||||
|
当前版本
|
||||||
|
<span class="font-mono text-foreground">
|
||||||
|
{{ updateStatus?.current_version || '加载中...' }}
|
||||||
|
</span>
|
||||||
|
<template v-if="updateStatus?.latest_version">
|
||||||
|
,最新版本
|
||||||
|
<span class="font-mono text-foreground">
|
||||||
|
{{ updateStatus.latest_version }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
v-if="updateStatus?.error"
|
||||||
|
class="mt-2 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
检查更新失败:{{ updateStatus.error }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
:disabled="loadingUpdateStatus"
|
||||||
|
@click="loadSystemUpdateStatus"
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
class="mr-2 h-3.5 w-3.5"
|
||||||
|
:class="loadingUpdateStatus ? 'animate-spin' : ''"
|
||||||
|
/>
|
||||||
|
重新检查
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="updateStatus?.has_update && updateStatus.release_url"
|
||||||
|
size="sm"
|
||||||
|
@click="openReleasePage"
|
||||||
|
>
|
||||||
|
<ExternalLink class="mr-2 h-3.5 w-3.5" />
|
||||||
|
查看更新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<!-- 管理员:系统健康摘要 -->
|
<!-- 管理员:系统健康摘要 -->
|
||||||
<div
|
<div
|
||||||
v-if="isAdmin && systemHealth"
|
v-if="isAdmin && systemHealth"
|
||||||
@@ -792,6 +863,7 @@
|
|||||||
import { ref, onMounted, computed, onBeforeUnmount, nextTick, watch } from 'vue'
|
import { ref, onMounted, computed, onBeforeUnmount, nextTick, watch } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { dashboardApi, type DashboardStat, type DailyStat, type ProviderSummary } from '@/api/dashboard'
|
import { dashboardApi, type DashboardStat, type DailyStat, type ProviderSummary } from '@/api/dashboard'
|
||||||
|
import { adminApi, type CheckUpdateResponse } from '@/api/admin'
|
||||||
import { getDateRangeFromPeriod } from '@/features/usage/composables'
|
import { getDateRangeFromPeriod } from '@/features/usage/composables'
|
||||||
import type { DateRangeParams } from '@/features/usage/types'
|
import type { DateRangeParams } from '@/features/usage/types'
|
||||||
import { announcementApi, type Announcement } from '@/api/announcements'
|
import { announcementApi, type Announcement } from '@/api/announcements'
|
||||||
@@ -828,10 +900,13 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
Clock,
|
Clock,
|
||||||
Database,
|
Database,
|
||||||
Shuffle
|
Shuffle,
|
||||||
|
RefreshCw,
|
||||||
|
ExternalLink
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||||
import { parseDateLike } from '@/utils/date'
|
import { parseDateLike } from '@/utils/date'
|
||||||
|
import { buildDashboardUpdateErrorStatus, describeDashboardUpdateStatus } from './dashboardUpdateStatus'
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import { sanitizeMarkdown } from '@/utils/sanitize'
|
import { sanitizeMarkdown } from '@/utils/sanitize'
|
||||||
import type { ChartData, ChartOptions, ChartDataset, TooltipItem } from 'chart.js'
|
import type { ChartData, ChartOptions, ChartDataset, TooltipItem } from 'chart.js'
|
||||||
@@ -970,11 +1045,17 @@ const cacheStats = ref<{
|
|||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
const userMonthlyCost = ref<number | null>(null)
|
const userMonthlyCost = ref<number | null>(null)
|
||||||
|
const updateStatus = ref<CheckUpdateResponse | null>(null)
|
||||||
|
const loadingUpdateStatus = ref(false)
|
||||||
|
|
||||||
const hasCacheData = computed(() =>
|
const hasCacheData = computed(() =>
|
||||||
cacheStats.value && cacheStats.value.total_cache_tokens > 0
|
cacheStats.value && cacheStats.value.total_cache_tokens > 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const updateStatusLabel = computed(() => {
|
||||||
|
return describeDashboardUpdateStatus(updateStatus.value)
|
||||||
|
})
|
||||||
|
|
||||||
const tokenBreakdown = ref<{
|
const tokenBreakdown = ref<{
|
||||||
input: number
|
input: number
|
||||||
output: number
|
output: number
|
||||||
@@ -1286,7 +1367,8 @@ onMounted(async () => {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadDashboardData(),
|
loadDashboardData(),
|
||||||
loadAnnouncements(),
|
loadAnnouncements(),
|
||||||
loadDailyStats()
|
loadDailyStats(),
|
||||||
|
loadSystemUpdateStatus()
|
||||||
])
|
])
|
||||||
await nextTick()
|
await nextTick()
|
||||||
setupTimelineResizeObserver()
|
setupTimelineResizeObserver()
|
||||||
@@ -1342,6 +1424,24 @@ async function loadDashboardData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadSystemUpdateStatus() {
|
||||||
|
if (!isAdmin.value) return
|
||||||
|
loadingUpdateStatus.value = true
|
||||||
|
try {
|
||||||
|
updateStatus.value = await adminApi.checkUpdate()
|
||||||
|
} catch (error) {
|
||||||
|
updateStatus.value = buildDashboardUpdateErrorStatus(updateStatus.value, error)
|
||||||
|
} finally {
|
||||||
|
loadingUpdateStatus.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openReleasePage() {
|
||||||
|
if (updateStatus.value?.release_url) {
|
||||||
|
window.open(updateStatus.value.release_url, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadDailyStats() {
|
async function loadDailyStats() {
|
||||||
if (dailyStatsLoadPromise) {
|
if (dailyStatsLoadPromise) {
|
||||||
hasPendingDailyStatsLoad = true
|
hasPendingDailyStatsLoad = true
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import type { CheckUpdateResponse } from '@/api/admin'
|
||||||
|
import {
|
||||||
|
buildDashboardUpdateErrorStatus,
|
||||||
|
describeDashboardUpdateStatus,
|
||||||
|
} from '../dashboardUpdateStatus'
|
||||||
|
|
||||||
|
function updateStatus(overrides: Partial<CheckUpdateResponse> = {}): CheckUpdateResponse {
|
||||||
|
return {
|
||||||
|
current_version: '0.7.0-rc27',
|
||||||
|
latest_version: null,
|
||||||
|
has_update: false,
|
||||||
|
release_url: null,
|
||||||
|
release_notes: null,
|
||||||
|
published_at: null,
|
||||||
|
error: null,
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('dashboardUpdateStatus', () => {
|
||||||
|
it('describes loading and latest states', () => {
|
||||||
|
expect(describeDashboardUpdateStatus(null)).toBe('检查中')
|
||||||
|
expect(describeDashboardUpdateStatus(updateStatus())).toBe('已是最新')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prioritizes update availability over latest-version text', () => {
|
||||||
|
expect(describeDashboardUpdateStatus(updateStatus({
|
||||||
|
latest_version: 'v0.7.0-rc28',
|
||||||
|
has_update: true,
|
||||||
|
release_url: 'https://github.com/fawney19/Aether/releases/tag/v0.7.0-rc28',
|
||||||
|
}))).toBe('有新版本')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves the current version when building an error state', () => {
|
||||||
|
const status = buildDashboardUpdateErrorStatus(
|
||||||
|
updateStatus({ current_version: '0.7.0-rc28' }),
|
||||||
|
new Error('network down')
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(status.current_version).toBe('0.7.0-rc28')
|
||||||
|
expect(status.has_update).toBe(false)
|
||||||
|
expect(status.error).toBe('network down')
|
||||||
|
})
|
||||||
|
})
|
||||||
23
frontend/src/views/shared/dashboardUpdateStatus.ts
Normal file
23
frontend/src/views/shared/dashboardUpdateStatus.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { CheckUpdateResponse } from '@/api/admin'
|
||||||
|
|
||||||
|
export function describeDashboardUpdateStatus(status: CheckUpdateResponse | null): string {
|
||||||
|
if (!status) return '检查中'
|
||||||
|
if (status.has_update) return '有新版本'
|
||||||
|
if (status.error) return '检查失败'
|
||||||
|
return '已是最新'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDashboardUpdateErrorStatus(
|
||||||
|
previousStatus: CheckUpdateResponse | null,
|
||||||
|
error: unknown
|
||||||
|
): CheckUpdateResponse {
|
||||||
|
return {
|
||||||
|
current_version: previousStatus?.current_version || '',
|
||||||
|
latest_version: null,
|
||||||
|
has_update: false,
|
||||||
|
release_url: null,
|
||||||
|
release_notes: null,
|
||||||
|
published_at: null,
|
||||||
|
error: error instanceof Error ? error.message : '检查更新失败'
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user