improve(settings): rework about support info and debug log layout

This commit is contained in:
zipg authored and GitHub committed 2026-09-12 17:45:11 +08:00
1 parent b6cfb3b728
commit 0e67027acf
13 files changed
+152 -41

No files matched your search

@@ -156,6 +156,7 @@ import {
webdavSyncSecretsStatus,
webdavSyncTest,
webdavSyncUpload,
listInstalledAgentsLocal,
type AppSupportInfo,
type McpHttpServerSettings,
type McpHttpServerStatus,
@@ -2229,9 +2230,30 @@ const appSupportInfoLabels = computed<AppSupportInfoLabels>(() => ({
runtimeWeb: t("settings.supportInfoRuntimeWeb"),
operatingSystem: t("settings.supportInfoOperatingSystem"),
architecture: t("settings.supportInfoArchitecture"),
userAgent: t("settings.supportInfoUserAgent"),
databaseTypes: t("settings.supportInfoDatabaseTypes"),
localDriverVersions: t("settings.supportInfoLocalDriverVersions"),
aiProviders: t("settings.supportInfoAiProviders"),
unknown: t("settings.supportInfoUnknown"),
}));
const appSupportInfoRows = computed(() => (appSupportInfo.value ? buildAppSupportInfoRows(appSupportInfo.value, appSupportInfoLabels.value) : []));
const appSupportInfoRows = computed(() => {
if (!appSupportInfo.value) return [];
const info = appSupportInfo.value;
return buildAppSupportInfoRows(info, appSupportInfoLabels.value).map((row) => {
const details: string[] = [];
if (row.key === "appVersion") {
if (info.databaseTypes?.length) details.push(`${appSupportInfoLabels.value.databaseTypes}: ${info.databaseTypes.join(", ")}`);
if (info.localDriverVersions?.length) {
details.push(`${appSupportInfoLabels.value.localDriverVersions}: ${info.localDriverVersions.map((driver) => `${driver.dbType} ${driver.version}`).join(", ")}`);
}
}
if (row.key === "operatingSystem") {
if (info.userAgent?.trim()) details.push(`${appSupportInfoLabels.value.userAgent}: ${info.userAgent.trim()}`);
if (info.aiProviders?.length) details.push(`${appSupportInfoLabels.value.aiProviders}: ${info.aiProviders.join(", ")}`);
}
return { ...row, tooltip: details.join("\n") };
});
});
const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[]>(() => [
{ value: "appearance", label: t("settings.appearanceTab") },
{ value: "editor", label: t("settings.editorTab") },
@@ -2457,7 +2479,33 @@ async function refreshAppSupportInfo() {
appSupportInfoLoading.value = true;
appSupportInfoError.value = "";
try {
appSupportInfo.value = await getAppSupportInfo();
const [info, localDrivers] = await Promise.all([getAppSupportInfo(), listInstalledAgentsLocal().catch(() => [])]);
const databaseTypes = [
...new Set(
connectionStore.connections
.map((connection) => {
const dbType = connection.db_type.trim();
const profile = connection.driver_profile?.trim();
return profile && profile !== dbType ? `${dbType}/${profile}` : dbType;
})
.filter(Boolean),
),
].sort();
const localDriverVersions = localDrivers
.filter((driver) => driver.installed && driver.installed_version?.trim())
.map((driver) => ({ dbType: driver.db_type, version: driver.installed_version!.trim() }))
.sort((a, b) => a.dbType.localeCompare(b.dbType));
const aiProviders = [
...new Set(
settingsStore.aiConfigs
.filter((config) => {
const preset = getAiProviderPreset(config.provider, config.endpoint);
return config.endpoint.trim() && config.model.trim() && (!preset.requiresApiKey || config.apiKey.trim());
})
.map((config) => getAiProviderPreset(config.provider, config.endpoint).label),
),
].sort();
appSupportInfo.value = { ...info, databaseTypes, localDriverVersions, aiProviders };
} catch (e: any) {
appSupportInfo.value = appSupportInfo.value || fallbackAppSupportInfo();
appSupportInfoError.value = e?.message || String(e);
@@ -6164,29 +6212,6 @@ onUnmounted(() => {
<Switch id="update-notifications-enabled" v-model="editUpdateNotificationsEnabled" />
</div>
<div v-if="!isWeb" class="settings-item flex flex-col gap-3 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center justify-between gap-4">
<div class="space-y-1">
<Label for="debug-logging-enabled">{{ t("settings.debugLoggingEnabled") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.debugLoggingEnabledDescription") }}
</p>
</div>
<Switch id="debug-logging-enabled" v-model="editDebugLoggingEnabled" />
</div>
<div class="flex justify-end gap-2">
<Button type="button" variant="outline" size="sm" @click="clearDebugLogs">
{{ t("settings.debugLogsClear") }}
</Button>
<Button type="button" variant="outline" size="sm" @click="copyDebugLogs">
{{ debugLogCopied ? t("settings.debugLogsCopied") : t("settings.debugLogsCopy") }}
</Button>
<Button type="button" variant="outline" size="sm" @click="exportDebugLogs">
{{ debugLogDownloaded ? t("settings.debugLogsDownloaded") : t("settings.debugLogsDownload") }}
</Button>
</div>
</div>
<div class="settings-appearance-group" data-icon-theme-settings>
<Label>{{ t("settings.iconTheme") }}</Label>
<div class="settings-appearance-choice-grid settings-icon-theme-grid">
@@ -9296,16 +9321,25 @@ LIMIT 100;</pre
</Button>
</div>
</div>
<div v-if="appSupportInfoRows.length" class="mt-4 grid gap-3 sm:grid-cols-2">
<div v-for="row in appSupportInfoRows" :key="row.key" class="min-w-0 rounded-md bg-muted/30 px-3 py-2">
<div class="text-xs font-medium text-muted-foreground">
{{ row.label }}
</div>
<div class="mt-1 min-w-0 select-text break-words font-mono text-xs text-foreground">
{{ row.value }}
</div>
<TooltipProvider v-if="appSupportInfoRows.length">
<div class="mt-4 grid gap-3 sm:grid-cols-4">
<Tooltip v-for="row in appSupportInfoRows" :key="row.key">
<TooltipTrigger as-child>
<div class="min-w-0 rounded-md bg-muted/30 px-3 py-2" :class="row.tooltip ? 'cursor-help' : ''">
<div class="text-xs font-medium text-muted-foreground">
{{ row.label }}
</div>
<div class="mt-1 min-w-0 select-text break-words font-mono text-xs text-foreground">
{{ row.value }}
</div>
</div>
</TooltipTrigger>
<TooltipContent v-if="row.tooltip" class="max-w-[520px] whitespace-pre-line break-words text-xs leading-relaxed" side="top" align="start" :side-offset="8">
{{ row.tooltip }}
</TooltipContent>
</Tooltip>
</div>
</div>
</TooltipProvider>
<p v-else class="mt-4 text-sm text-muted-foreground">
{{ t("settings.supportInfoLoading") }}
</p>
@@ -9326,7 +9360,28 @@ LIMIT 100;</pre
:get-draft-conflict-categories="getDraftConflictCategories"
/>
<ChangelogPanel :checking-updates="props.checkingUpdates" @check-updates="emit('check-updates')" />
<div v-if="!isWeb" class="settings-item flex flex-col gap-3 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center justify-between gap-4">
<div class="space-y-1">
<Label for="debug-logging-enabled">{{ t("settings.debugLoggingEnabled") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.debugLoggingEnabledDescription") }}
</p>
</div>
<Switch id="debug-logging-enabled" v-model="editDebugLoggingEnabled" />
</div>
<div class="flex justify-end gap-2">
<Button type="button" variant="outline" size="sm" @click="clearDebugLogs">
{{ t("settings.debugLogsClear") }}
</Button>
<Button type="button" variant="outline" size="sm" @click="copyDebugLogs">
{{ debugLogCopied ? t("settings.debugLogsCopied") : t("settings.debugLogsCopy") }}
</Button>
<Button type="button" variant="outline" size="sm" @click="exportDebugLogs">
{{ debugLogDownloaded ? t("settings.debugLogsDownloaded") : t("settings.debugLogsDownload") }}
</Button>
</div>
</div>
<div class="settings-item rounded-lg border p-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
@@ -9348,6 +9403,8 @@ LIMIT 100;</pre
</div>
</div>
<ChangelogPanel :checking-updates="props.checkingUpdates" @check-updates="emit('check-updates')" />
<div class="grid gap-3 sm:grid-cols-2">
<button type="button" class="rounded-lg border p-4 text-left transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" @click="openExternalUrl('https://qm.qq.com/cgi-bin/qm/qr?k=&group_code=1087880322')">
<div class="text-xs font-medium uppercase tracking-wider text-muted-foreground">
@@ -128,8 +128,8 @@ describe("EditorSettingsDialog choice card containment", () => {
}
});
it("restores large icon theme choices below the debug log controls", () => {
expect(sourceIndexForKey("iconTheme")).toBeGreaterThan(sourceIndexForKey("debugLoggingEnabled"));
it("keeps large icon theme choices in appearance and debug logs in About", () => {
expect(sourceIndexForKey("iconTheme")).toBeLessThan(sourceIndexForKey("debugLoggingEnabled"));
expect(templateSource).toContain("data-icon-theme-settings");
for (const key of ["iconThemeDefault", "iconThemeBlack"] as const) {
const block = buttonBlockForKey(key);
+4
View File
@@ -7716,6 +7716,10 @@ export default {
supportInfoRuntimeWeb: "Web",
supportInfoOperatingSystem: "Operating System",
supportInfoArchitecture: "Architecture",
supportInfoUserAgent: "User-Agent",
supportInfoDatabaseTypes: "Database Types",
supportInfoLocalDriverVersions: "Local Driver Versions",
supportInfoAiProviders: "AI Providers",
supportInfoUnknown: "Unknown",
settingsTransferTitle: "Import & export settings",
settingsTransferDescription: "Export app settings to a file to back up your configuration, or restore settings from a backup file.",
+4
View File
@@ -7332,6 +7332,10 @@ export default withEnglishFallback({
mcpQueryTimeoutSaveStatus_saving: "Guardando...",
mcpQueryTimeoutSaveStatus_saved: "Guardado",
mcpQueryTimeoutSaveStatus_failed: "Error al guardar",
supportInfoUserAgent: "User-Agent",
supportInfoDatabaseTypes: "Tipos de bases de datos",
supportInfoLocalDriverVersions: "Versiones de controladores locales",
supportInfoAiProviders: "Proveedores de IA",
},
driverStore: {
jreDirRemoveFailed: "No se pudo eliminar el directorio JRE antiguo: {path} (error original: {error})",
+4
View File
@@ -7332,6 +7332,10 @@ export default withEnglishFallback({
mcpQueryTimeoutSaveStatus_saving: "Salvataggio in corso...",
mcpQueryTimeoutSaveStatus_saved: "Salvato",
mcpQueryTimeoutSaveStatus_failed: "Salvataggio non riuscito",
supportInfoUserAgent: "User-Agent",
supportInfoDatabaseTypes: "Tipi di database",
supportInfoLocalDriverVersions: "Versioni dei driver locali",
supportInfoAiProviders: "Provider AI",
},
driverStore: {
jreDirRemoveFailed: "Impossibile rimuovere la vecchia directory JRE: {path} (errore originale: {error})",
+4
View File
@@ -7384,6 +7384,10 @@ export default withEnglishFallback({
mcpQueryTimeoutSaveStatus_saving: "保存中...",
mcpQueryTimeoutSaveStatus_saved: "保存済み",
mcpQueryTimeoutSaveStatus_failed: "保存失敗",
supportInfoUserAgent: "User-Agent",
supportInfoDatabaseTypes: "データベースの種類",
supportInfoLocalDriverVersions: "ローカルドライバーのバージョン",
supportInfoAiProviders: "AIプロバイダー",
},
driverStore: {
jreDirRemoveFailed: "古い JRE ディレクトリを削除できませんでした: {path}(元のエラー: {error})",
+4
View File
@@ -7056,6 +7056,10 @@ export default withEnglishFallback({
mcpQueryTimeoutSaveStatus_saving: "저장 중...",
mcpQueryTimeoutSaveStatus_saved: "저장됨",
mcpQueryTimeoutSaveStatus_failed: "저장 실패",
supportInfoUserAgent: "User-Agent",
supportInfoDatabaseTypes: "데이터베이스 유형",
supportInfoLocalDriverVersions: "로컬 드라이버 버전",
supportInfoAiProviders: "AI 제공자",
},
driverStore: {
jreDirRemoveFailed: "이전 JRE 디렉터리 제거 실패: {path} (원래 오류: {error})",
+4
View File
@@ -7334,6 +7334,10 @@ export default withEnglishFallback({
mcpQueryTimeoutSaveStatus_saving: "Salvando...",
mcpQueryTimeoutSaveStatus_saved: "Salvo",
mcpQueryTimeoutSaveStatus_failed: "Falha ao salvar",
supportInfoUserAgent: "User-Agent",
supportInfoDatabaseTypes: "Tipos de banco de dados",
supportInfoLocalDriverVersions: "Versões de drivers locais",
supportInfoAiProviders: "Provedores de IA",
},
driverStore: {
jreDirRemoveFailed: "Não foi possível remover o diretório JRE antigo: {path} (erro original: {error})",
+4
View File
@@ -7701,6 +7701,10 @@ export default withEnglishFallback({
supportInfoRuntimeWeb: "Web 端",
supportInfoOperatingSystem: "操作系统",
supportInfoArchitecture: "系统架构",
supportInfoUserAgent: "User-Agent",
supportInfoDatabaseTypes: "数据库类型",
supportInfoLocalDriverVersions: "本地驱动版本",
supportInfoAiProviders: "AI 供应商",
supportInfoUnknown: "未知",
settingsTransferTitle: "配置导入与导出",
settingsTransferDescription: "导出应用设置到文件进行配置备份,或从备份文件恢复配置。",
+4
View File
@@ -6649,6 +6649,10 @@ export default withEnglishFallback({
mcpQueryTimeoutSaveStatus_saving: "儲存中...",
mcpQueryTimeoutSaveStatus_saved: "已儲存",
mcpQueryTimeoutSaveStatus_failed: "儲存失敗",
supportInfoUserAgent: "User-Agent",
supportInfoDatabaseTypes: "資料庫類型",
supportInfoLocalDriverVersions: "本機驅動程式版本",
supportInfoAiProviders: "AI 供應商",
},
driverStore: {
jreDirRemoveFailed: "無法刪除舊的 JRE 目錄:{path}(原始錯誤:{error})",
+17 -3
View File
@@ -7,6 +7,10 @@ export interface AppSupportInfoLabels {
runtimeWeb: string;
operatingSystem: string;
architecture: string;
userAgent: string;
databaseTypes: string;
localDriverVersions: string;
aiProviders: string;
unknown: string;
}
@@ -35,6 +39,10 @@ export function formatSupportInfoOperatingSystem(info: AppSupportInfo, unknownLa
return [name, version].filter(Boolean).join(" ");
}
export function collectBrowserSupportInfo(): string {
return typeof navigator === "undefined" ? "" : navigator.userAgent?.trim() || "";
}
export function buildAppSupportInfoRows(info: AppSupportInfo, labels: AppSupportInfoLabels): AppSupportInfoRow[] {
return [
{
@@ -61,7 +69,13 @@ export function buildAppSupportInfoRows(info: AppSupportInfo, labels: AppSupport
}
export function formatAppSupportInfoForClipboard(info: AppSupportInfo, labels: AppSupportInfoLabels): string {
return buildAppSupportInfoRows(info, labels)
.map((row) => `${row.label}: ${row.value}`)
.join("\n");
const rows = buildAppSupportInfoRows(info, labels).map((row) => `${row.label}: ${row.value}`);
const extraRows: Array<[string, string | null | undefined]> = [];
if (info.userAgent?.trim()) extraRows.push([labels.userAgent, info.userAgent]);
if (info.databaseTypes?.length) extraRows.push([labels.databaseTypes, info.databaseTypes.join(", ")]);
if (info.localDriverVersions?.length) {
extraRows.push([labels.localDriverVersions, info.localDriverVersions.map((driver) => `${driver.dbType} ${driver.version}`).join(", ")]);
}
if (info.aiProviders?.length) extraRows.push([labels.aiProviders, info.aiProviders.join(", ")]);
return rows.concat(extraRows.map(([label, value]) => `${label}: ${value?.trim() || labels.unknown}`)).join("\n");
}
+2
View File
@@ -247,6 +247,7 @@ import type {
} from "@/types/nacos";
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog";
import { collectBrowserSupportInfo } from "@/lib/app/supportInfo";
import { normalizeConnectionTestResult } from "@/lib/connection/connectionDatabaseInfo";
import type { AnnotationFile, SchemaSnapshot } from "@/docs/types";
@@ -4526,6 +4527,7 @@ export async function getAppSupportInfo(): Promise<AppSupportInfo> {
osName: navigator.platform || "web",
osVersion: null,
arch: "",
userAgent: collectBrowserSupportInfo(),
};
}
+7 -1
View File
@@ -1,5 +1,6 @@
import { invoke as tauriInvoke } from "@tauri-apps/api/core";
import { assertUpdateAllowsCommand } from "@/lib/app/updatePreparation";
import { collectBrowserSupportInfo } from "@/lib/app/supportInfo";
function invoke<T>(command: string, args?: Record<string, unknown>): Promise<T> {
assertUpdateAllowsCommand(command);
@@ -371,6 +372,10 @@ export interface AppSupportInfo {
osName: string;
osVersion?: string | null;
arch: string;
userAgent?: string;
databaseTypes?: string[];
localDriverVersions?: Array<{ dbType: string; version: string }>;
aiProviders?: string[];
}
export interface QueryPagination {
@@ -2559,7 +2564,8 @@ export async function getAppVersion(): Promise<string> {
}
export async function getAppSupportInfo(): Promise<AppSupportInfo> {
return invoke<AppSupportInfo>("get_app_support_info");
const info = await invoke<AppSupportInfo>("get_app_support_info");
return { ...info, userAgent: collectBrowserSupportInfo() };
}
// --- Redis ---