diff --git a/mateclaw-server/src/main/java/vip/mate/config/DatabaseBootstrapRunner.java b/mateclaw-server/src/main/java/vip/mate/config/DatabaseBootstrapRunner.java index 3472e7c6..ebd1431c 100644 --- a/mateclaw-server/src/main/java/vip/mate/config/DatabaseBootstrapRunner.java +++ b/mateclaw-server/src/main/java/vip/mate/config/DatabaseBootstrapRunner.java @@ -47,6 +47,9 @@ public class DatabaseBootstrapRunner implements ApplicationRunner { /** Cached flag: true when running on PostgreSQL. */ private volatile Boolean isPostgres; + /** Cached human-readable label of the connected database, e.g. "MySQL" / "H2" / "PostgreSQL". */ + private volatile String databaseLabel; + /** * When true, wait for Desktop splash screen to call /setup/init with chosen language. * When false (default), auto-initialize immediately on startup. @@ -150,6 +153,57 @@ public class DatabaseBootstrapRunner implements ApplicationRunner { } } + /** + * Friendly product name of the currently connected database, e.g. {@code "MySQL"}, + * {@code "PostgreSQL"}, {@code "H2"} or {@code "KingbaseES"}. Read once from JDBC + * metadata and cached — the connected database never changes at runtime. + * + * @return the product name, or {@code "Unknown"} if metadata is unavailable. + */ + public String getDatabaseLabel() { + if (databaseLabel == null) { + try (Connection connection = dataSource.getConnection()) { + databaseLabel = normalizeDatabaseLabel(connection.getMetaData().getDatabaseProductName()); + } catch (Exception e) { + log.debug("Failed to read database product name: {}", e.getMessage()); + databaseLabel = "Unknown"; + } + } + return databaseLabel; + } + + /** + * Maps a raw JDBC product name to a clean, canonical label. Some drivers append + * version noise to the product name (e.g. KingbaseES reports "KingbaseES V008R006"); + * collapsing on a keyword keeps the displayed label stable across driver versions + * and consistent with the dialect this runner detects for DDL. + * + * @return a canonical label, or {@code "Unknown"} when the product name is absent. + */ + static String normalizeDatabaseLabel(String product) { + if (product == null || product.isBlank()) { + return "Unknown"; + } + String lower = product.toLowerCase(); + if (lower.contains("kingbase")) { + // KingbaseES is the product name; show the vendor's Chinese brand name. + return "人大金仓"; + } + if (lower.contains("mariadb")) { + return "MariaDB"; + } + if (lower.contains("mysql")) { + return "MySQL"; + } + if (lower.contains("postgresql")) { + return "PostgreSQL"; + } + if (lower.contains("h2")) { + return "H2"; + } + return product.trim(); + } + private boolean tableExists(String tableName) throws Exception { try (Connection connection = dataSource.getConnection()) { DatabaseMetaData metaData = connection.getMetaData(); diff --git a/mateclaw-server/src/main/java/vip/mate/system/service/SystemHealthService.java b/mateclaw-server/src/main/java/vip/mate/system/service/SystemHealthService.java index 1128ffb4..b886af16 100644 --- a/mateclaw-server/src/main/java/vip/mate/system/service/SystemHealthService.java +++ b/mateclaw-server/src/main/java/vip/mate/system/service/SystemHealthService.java @@ -67,7 +67,7 @@ public class SystemHealthService { } } - return new HealthResponse(overall, checks); + return new HealthResponse(overall, checks, bootstrapRunner.getDatabaseLabel()); } private HealthCheck checkDefaultModel() { @@ -190,7 +190,7 @@ public class SystemHealthService { // ==================== Response Records ==================== - public record HealthResponse(String overall, List checks) {} + public record HealthResponse(String overall, List checks, String database) {} public record HealthCheck(String name, String status, String message, HealthAction action) {} diff --git a/mateclaw-server/src/test/java/vip/mate/config/DatabaseBootstrapRunnerLabelTest.java b/mateclaw-server/src/test/java/vip/mate/config/DatabaseBootstrapRunnerLabelTest.java new file mode 100644 index 00000000..2d43e361 --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/config/DatabaseBootstrapRunnerLabelTest.java @@ -0,0 +1,44 @@ +package vip.mate.config; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for {@link DatabaseBootstrapRunner#normalizeDatabaseLabel(String)}. + * Verifies that raw JDBC product names — including version-suffixed ones from the + * KingbaseES / PostgreSQL family — collapse to a clean, canonical label. + */ +class DatabaseBootstrapRunnerLabelTest { + + @Test + @DisplayName("KingbaseES product name (with version noise) → '人大金仓'") + void kingbaseNormalizes() { + assertEquals("人大金仓", DatabaseBootstrapRunner.normalizeDatabaseLabel("KingbaseES")); + assertEquals("人大金仓", DatabaseBootstrapRunner.normalizeDatabaseLabel("KingbaseES V008R006")); + assertEquals("人大金仓", DatabaseBootstrapRunner.normalizeDatabaseLabel("kingbasees")); + } + + @Test + @DisplayName("MySQL / MariaDB → canonical labels") + void mysqlFamilyNormalizes() { + assertEquals("MySQL", DatabaseBootstrapRunner.normalizeDatabaseLabel("MySQL")); + assertEquals("MariaDB", DatabaseBootstrapRunner.normalizeDatabaseLabel("MariaDB")); + } + + @Test + @DisplayName("PostgreSQL and H2 → canonical labels") + void postgresAndH2Normalize() { + assertEquals("PostgreSQL", DatabaseBootstrapRunner.normalizeDatabaseLabel("PostgreSQL")); + assertEquals("H2", DatabaseBootstrapRunner.normalizeDatabaseLabel("H2")); + } + + @Test + @DisplayName("Unknown / blank product name → 'Unknown'; unrecognized name passes through trimmed") + void fallbacks() { + assertEquals("Unknown", DatabaseBootstrapRunner.normalizeDatabaseLabel(null)); + assertEquals("Unknown", DatabaseBootstrapRunner.normalizeDatabaseLabel(" ")); + assertEquals("Oracle", DatabaseBootstrapRunner.normalizeDatabaseLabel(" Oracle ")); + } +} diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index bcc38be5..0e6ace05 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -573,6 +573,7 @@ export default { doctor: { title: 'System Diagnostics', subtitle: 'Local instance health snapshot', + database: 'Connected database', checking: 'Checking...', allGood: 'All systems healthy', hasWarnings: '{count} warning(s)', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index ab12d221..fa2064eb 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -2054,6 +2054,7 @@ export default { doctor: { title: '系统诊断', subtitle: '本地实例健康概览', + database: '当前数据库', checking: '检查中...', allGood: '所有系统正常', hasWarnings: '{count} 个警告', diff --git a/mateclaw-ui/src/views/Dashboard.vue b/mateclaw-ui/src/views/Dashboard.vue index d6e10112..73d705b1 100644 --- a/mateclaw-ui/src/views/Dashboard.vue +++ b/mateclaw-ui/src/views/Dashboard.vue @@ -7,6 +7,11 @@
{{ t('dashboard.kicker') }}

{{ t('dashboard.title') }}

{{ t('dashboard.desc') }}

+
+ + {{ t('doctor.database') }} + {{ dbLabel }} +
{{ t('dashboard.periods.today') }}
@@ -190,7 +195,7 @@ import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' import { ArrowRight, ChatDotRound, DataLine, Document, Tools } from '@element-plus/icons-vue' -import { dashboardApi, modelApi } from '@/api' +import { dashboardApi, modelApi, http } from '@/api' import { getProviderIcon, onProviderIconError } from '@/utils/providerIcons' import * as echarts from 'echarts/core' import { LineChart } from 'echarts/charts' @@ -219,6 +224,9 @@ const todayStats = reactive({ // ── Model configuration card ── const modelProviders = ref([]) const activeModel = ref<{ providerId: string; model: string } | null>(null) +// Connected database product name (e.g. "MySQL" / "H2" / "PostgreSQL"), surfaced +// as a subtle line in the page header. Empty string hides it when unavailable. +const dbLabel = ref('') const readyProviderCount = computed( () => modelProviders.value.filter((p) => providerChipStatus(p) === 'ready').length, @@ -272,6 +280,15 @@ onMounted(async () => { // Dashboard data is non-critical } + // Connected database label — independent and non-critical. Reuses the + // existing system health endpoint, which already reports the product name. + try { + const healthRes: any = await http.get('/system/health') + dbLabel.value = (healthRes?.data || healthRes)?.database || '' + } catch { + dbLabel.value = '' + } + // Model configuration card — loaded independently so a failure here never // blanks the analytics above, and vice versa. try { @@ -415,6 +432,34 @@ function calcDuration(run: any): string { padding-right: 4px; } +.db-chip { + display: inline-flex; + align-items: center; + gap: 6px; + margin-top: 14px; + padding: 4px 10px; + border: 1px solid var(--mc-border); + border-radius: 999px; + background: var(--mc-bg-sunken); + font-size: 12px; + line-height: 1; + color: var(--mc-text-secondary); +} + +.db-chip__icon { + color: var(--mc-text-tertiary); + flex-shrink: 0; +} + +.db-chip__label { + color: var(--mc-text-tertiary); +} + +.db-chip__value { + font-weight: 600; + color: var(--mc-text-primary); +} + .hero-note { min-width: 220px; padding: 16px 18px;