diff --git a/src/components/api-server/ApiServerPage.tsx b/src/components/api-server/ApiServerPage.tsx
index 5989a69b8..0b5009291 100644
--- a/src/components/api-server/ApiServerPage.tsx
+++ b/src/components/api-server/ApiServerPage.tsx
@@ -902,6 +902,43 @@ export function ApiServerPage({ hideHeader = false }: ApiServerPageProps) {
),
[status, editHost],
);
+ const cacheHitRate = useMemo(() => {
+ const hits = status?.response_cache?.hits ?? 0;
+ const misses = status?.response_cache?.misses ?? 0;
+ const total = hits + misses;
+ if (total === 0) return 0;
+ return (hits / total) * 100;
+ }, [status]);
+ const capabilityExcludedReasonTotal = useMemo(() => {
+ const capability = status?.capability_routing;
+ if (!capability) return 0;
+ return (
+ capability.filter_excluded_tools_total +
+ capability.filter_excluded_vision_total +
+ capability.filter_excluded_context_total
+ );
+ }, [status]);
+ const requestDedupReplayRate = useMemo(() => {
+ const dedup = status?.request_dedup;
+ if (!dedup) return 0;
+ const totalChecks =
+ dedup.check_new_total +
+ dedup.check_in_progress_total +
+ dedup.check_completed_total;
+ const replayCount = dedup.check_completed_total + dedup.wait_success_total;
+ if (totalChecks === 0) return 0;
+ return (replayCount / totalChecks) * 100;
+ }, [status]);
+ const idempotencyReplayRate = useMemo(() => {
+ const idempotency = status?.idempotency;
+ if (!idempotency) return 0;
+ const totalChecks =
+ idempotency.check_new_total +
+ idempotency.check_in_progress_total +
+ idempotency.check_completed_total;
+ if (totalChecks === 0) return 0;
+ return (idempotency.check_completed_total / totalChecks) * 100;
+ }, [status]);
const handleGatewayModeChange = async (mode: GatewayMode) => {
const nextHost = mode === "local" ? "127.0.0.1" : "0.0.0.0";
@@ -1313,6 +1350,182 @@ export function ApiServerPage({ hideHeader = false }: ApiServerPageProps) {
)}
+ {/* 观测面板(对标 ClawRouter) */}
+
diff --git a/src/components/settings-v2/_layout/index.tsx b/src/components/settings-v2/_layout/index.tsx
index 87786dbc3..bbcaae238 100644
--- a/src/components/settings-v2/_layout/index.tsx
+++ b/src/components/settings-v2/_layout/index.tsx
@@ -5,7 +5,7 @@
* 参考成熟产品的设置布局设计
*/
-import { useState, ReactNode, useEffect } from "react";
+import { useState, ReactNode, useEffect, useRef } from "react";
import styled from "styled-components";
import { SettingsSidebar } from "./SettingsSidebar";
import { SettingsTabs } from "@/types/settings";
@@ -323,6 +323,7 @@ export function SettingsLayoutV2({
const [activeTab, setActiveTab] = useState
(
initialTab || SettingsTabs.Appearance,
);
+ const contentContainerRef = useRef(null);
useEffect(() => {
if (initialTab) {
@@ -330,6 +331,10 @@ export function SettingsLayoutV2({
}
}, [initialTab]);
+ useEffect(() => {
+ contentContainerRef.current?.scrollTo({ top: 0, behavior: "auto" });
+ }, [activeTab]);
+
const handleBackToHome = () => {
if (onNavigate) {
onNavigate("agent", buildHomeAgentParams());
@@ -345,7 +350,7 @@ export function SettingsLayoutV2({
{/* 设置内容 */}
-
+
{renderSettingsContent(activeTab)}
diff --git a/src/components/settings-v2/system/about/index.tsx b/src/components/settings-v2/system/about/index.tsx
index 9e5822cc5..5fae40a28 100644
--- a/src/components/settings-v2/system/about/index.tsx
+++ b/src/components/settings-v2/system/about/index.tsx
@@ -23,6 +23,8 @@ interface DownloadResult {
filePath?: string;
}
+const FALLBACK_TAGS_URL = "https://github.com/aiclientproxy/proxycast/tags";
+
const CREATIVE_THEMES = [
"通用对话",
"社媒内容",
@@ -63,6 +65,7 @@ export function AboutSection() {
const [downloadResult, setDownloadResult] = useState(
null,
);
+ const manualDownloadUrl = versionInfo.downloadUrl || FALLBACK_TAGS_URL;
// 加载当前版本号(从后端获取,确保与 Cargo.toml 同步)
useEffect(() => {
@@ -73,9 +76,14 @@ export function AboutSection() {
setVersionInfo((prev) => ({
...prev,
current: result.current,
+ downloadUrl: result.downloadUrl || FALLBACK_TAGS_URL,
}));
} catch (error) {
console.error("Failed to load version:", error);
+ setVersionInfo((prev) => ({
+ ...prev,
+ downloadUrl: prev.downloadUrl || FALLBACK_TAGS_URL,
+ }));
}
};
loadCurrentVersion();
@@ -86,12 +94,16 @@ export function AboutSection() {
setDownloadResult(null);
try {
const result = await safeInvoke("check_for_updates");
- setVersionInfo(result);
+ setVersionInfo({
+ ...result,
+ downloadUrl: result.downloadUrl || FALLBACK_TAGS_URL,
+ });
} catch (error) {
console.error("Failed to check for updates:", error);
setVersionInfo((prev) => ({
...prev,
error: t("检查更新失败", "检查更新失败"),
+ downloadUrl: prev.downloadUrl || FALLBACK_TAGS_URL,
}));
} finally {
setChecking(false);
@@ -124,7 +136,7 @@ export function AboutSection() {
console.error("Failed to download update:", error);
setDownloadResult({
success: false,
- message: t("下载失败,请手动下载", "下载失败,请手动下载"),
+ message: t("下载失败,请手动下载最新版", "下载失败,请手动下载最新版"),
filePath: undefined,
});
} finally {
@@ -207,17 +219,15 @@ export function AboutSection() {
: t("下载更新", "下载更新")}
- {versionInfo.downloadUrl && (
-
-
- {t("网页下载", "网页下载")}
-
- )}
+
+
+ {t("网页下载", "网页下载")}
+
>
)}
@@ -239,14 +249,14 @@ export function AboutSection() {
)}
{downloadResult.message}
- {!downloadResult.success && versionInfo.downloadUrl && (
+ {!downloadResult.success && (
-
+ className="inline-flex items-center gap-1 mt-2 underline hover:no-underline"
+ >
+
{t("前往网页下载", "前往网页下载")}
)}
diff --git a/src/components/settings-v2/system/developer/index.tsx b/src/components/settings-v2/system/developer/index.tsx
index f0187b264..61548ee95 100644
--- a/src/components/settings-v2/system/developer/index.tsx
+++ b/src/components/settings-v2/system/developer/index.tsx
@@ -6,14 +6,18 @@ import { useCallback, useState } from "react";
import { Bug, Code2, Eye } from "lucide-react";
import { Switch } from "@/components/ui/switch";
import { useComponentDebug } from "@/contexts/ComponentDebugContext";
-import { getConfig, getLogs } from "@/hooks/useTauri";
+import { getConfig, getLogs, getPersistedLogsTail } from "@/hooks/useTauri";
import {
buildCrashDiagnosticPayload,
copyCrashDiagnosticToClipboard,
+ copyCrashDiagnosticJsonToClipboard,
exportCrashDiagnosticToJson,
+ isClipboardPermissionDeniedError,
normalizeCrashReportingConfig,
+ openCrashDiagnosticDownloadDirectory,
} from "@/lib/crashDiagnostic";
import { cn } from "@/lib/utils";
+import { ClipboardPermissionGuideCard } from "../shared/ClipboardPermissionGuideCard";
export function DeveloperSettings() {
const { enabled, setEnabled } = useComponentDebug();
@@ -22,12 +26,18 @@ export function DeveloperSettings() {
type: "success" | "error";
text: string;
} | null>(null);
+ const [showClipboardGuide, setShowClipboardGuide] = useState(false);
const buildDiagnosticPayload = useCallback(async () => {
- const [config, logs] = await Promise.all([getConfig(), getLogs()]);
+ const [config, logs, persistedLogs] = await Promise.all([
+ getConfig(),
+ getLogs(),
+ getPersistedLogsTail(200),
+ ]);
return buildCrashDiagnosticPayload({
crashConfig: normalizeCrashReportingConfig(config.crash_reporting),
logs,
+ persistedLogTail: persistedLogs,
appVersion: import.meta.env.VITE_APP_VERSION,
platform: navigator.platform,
userAgent: navigator.userAgent,
@@ -37,6 +47,7 @@ export function DeveloperSettings() {
const handleCopyDiagnostic = useCallback(async () => {
setDiagnosticBusy(true);
setMessage(null);
+ setShowClipboardGuide(false);
try {
const payload = await buildDiagnosticPayload();
await copyCrashDiagnosticToClipboard(payload);
@@ -47,6 +58,8 @@ export function DeveloperSettings() {
setTimeout(() => setMessage(null), 2500);
} catch (err) {
console.error("复制诊断信息失败:", err);
+ const isPermissionDenied = isClipboardPermissionDeniedError(err);
+ setShowClipboardGuide(isPermissionDenied);
setMessage({
type: "error",
text: err instanceof Error ? err.message : "复制诊断信息失败",
@@ -56,15 +69,52 @@ export function DeveloperSettings() {
}
}, [buildDiagnosticPayload]);
+ const handleCopyDiagnosticJson = useCallback(async () => {
+ setDiagnosticBusy(true);
+ setMessage(null);
+ setShowClipboardGuide(false);
+ try {
+ const payload = await buildDiagnosticPayload();
+ await copyCrashDiagnosticJsonToClipboard(payload);
+ setMessage({
+ type: "success",
+ text: "纯 JSON 诊断信息已复制",
+ });
+ setTimeout(() => setMessage(null), 2500);
+ } catch (err) {
+ console.error("复制纯 JSON 失败:", err);
+ const isPermissionDenied = isClipboardPermissionDeniedError(err);
+ setShowClipboardGuide(isPermissionDenied);
+ setMessage({
+ type: "error",
+ text: err instanceof Error ? err.message : "复制纯 JSON 失败",
+ });
+ } finally {
+ setDiagnosticBusy(false);
+ }
+ }, [buildDiagnosticPayload]);
+
const handleExportDiagnostic = useCallback(async () => {
setDiagnosticBusy(true);
setMessage(null);
+ setShowClipboardGuide(false);
try {
const payload = await buildDiagnosticPayload();
- exportCrashDiagnosticToJson(payload);
+ const result = exportCrashDiagnosticToJson(payload, {
+ sceneTag: "settings-developer",
+ });
+ let openedPath: string | null = null;
+ try {
+ const opened = await openCrashDiagnosticDownloadDirectory();
+ openedPath = opened.openedPath;
+ } catch {
+ openedPath = null;
+ }
setMessage({
type: "success",
- text: "诊断文件已导出,可发送给开发者",
+ text: openedPath
+ ? `诊断文件已导出:${result.fileName},并已打开目录:${openedPath}`
+ : `诊断文件已导出:${result.fileName}(位置:${result.locationHint})`,
});
setTimeout(() => setMessage(null), 2500);
} catch (err) {
@@ -78,6 +128,27 @@ export function DeveloperSettings() {
}
}, [buildDiagnosticPayload]);
+ const handleOpenDownloadDirectory = useCallback(async () => {
+ setDiagnosticBusy(true);
+ setMessage(null);
+ try {
+ const result = await openCrashDiagnosticDownloadDirectory();
+ setMessage({
+ type: "success",
+ text: `已打开下载目录:${result.openedPath}`,
+ });
+ setTimeout(() => setMessage(null), 2500);
+ } catch (err) {
+ console.error("打开下载目录失败:", err);
+ setMessage({
+ type: "error",
+ text: err instanceof Error ? err.message : "打开下载目录失败",
+ });
+ } finally {
+ setDiagnosticBusy(false);
+ }
+ }, []);
+
return (
{/* 标题 */}
@@ -100,6 +171,8 @@ export function DeveloperSettings() {
)}
+ {showClipboardGuide &&
}
+
{/* 组件视图调试 */}
@@ -155,6 +228,17 @@ export function DeveloperSettings() {
>
复制诊断信息
+
+
diff --git a/src/components/settings-v2/system/experimental/index.tsx b/src/components/settings-v2/system/experimental/index.tsx
index 2c6b0c7d2..a0aa6720f 100644
--- a/src/components/settings-v2/system/experimental/index.tsx
+++ b/src/components/settings-v2/system/experimental/index.tsx
@@ -19,6 +19,7 @@ import {
saveConfig,
type Config,
getLogs,
+ getPersistedLogsTail,
type CrashReportingConfig,
} from "@/hooks/useTauri";
import { ShortcutSettings } from "@/components/smart-input/ShortcutSettings";
@@ -32,11 +33,15 @@ import {
import { applyCrashReportingSettings } from "@/lib/crashReporting";
import {
buildCrashDiagnosticPayload,
+ copyCrashDiagnosticJsonToClipboard,
copyCrashDiagnosticToClipboard,
DEFAULT_CRASH_REPORTING_CONFIG,
exportCrashDiagnosticToJson,
+ isClipboardPermissionDeniedError,
normalizeCrashReportingConfig,
+ openCrashDiagnosticDownloadDirectory,
} from "@/lib/crashDiagnostic";
+import { ClipboardPermissionGuideCard } from "../shared/ClipboardPermissionGuideCard";
// ============================================================
// 组件
@@ -57,6 +62,7 @@ export function ExperimentalSettings() {
type: "success" | "error";
text: string;
} | null>(null);
+ const [showClipboardGuide, setShowClipboardGuide] = useState(false);
// 检测是否为 macOS(使用 userAgentData 或 userAgent 替代已弃用的 platform)
const isMacOS = navigator.userAgent.includes("Mac");
@@ -253,10 +259,14 @@ export function ExperimentalSettings() {
}, [crashConfig, persistCrashConfig]);
const buildDiagnosticPayload = useCallback(async () => {
- const logs = await getLogs();
+ const [logs, persistedLogs] = await Promise.all([
+ getLogs(),
+ getPersistedLogsTail(200),
+ ]);
return buildCrashDiagnosticPayload({
crashConfig,
logs,
+ persistedLogTail: persistedLogs,
appVersion: import.meta.env.VITE_APP_VERSION,
platform: navigator.platform,
userAgent: navigator.userAgent,
@@ -266,6 +276,7 @@ export function ExperimentalSettings() {
const copyCrashDiagnostic = useCallback(async () => {
setDiagnosticBusy(true);
setMessage(null);
+ setShowClipboardGuide(false);
try {
const payload = await buildDiagnosticPayload();
await copyCrashDiagnosticToClipboard(payload);
@@ -276,6 +287,8 @@ export function ExperimentalSettings() {
setTimeout(() => setMessage(null), 2500);
} catch (err) {
console.error("复制诊断信息失败:", err);
+ const isPermissionDenied = isClipboardPermissionDeniedError(err);
+ setShowClipboardGuide(isPermissionDenied);
setMessage({
type: "error",
text: err instanceof Error ? err.message : "复制诊断信息失败",
@@ -285,15 +298,52 @@ export function ExperimentalSettings() {
}
}, [buildDiagnosticPayload]);
+ const copyCrashDiagnosticJson = useCallback(async () => {
+ setDiagnosticBusy(true);
+ setMessage(null);
+ setShowClipboardGuide(false);
+ try {
+ const payload = await buildDiagnosticPayload();
+ await copyCrashDiagnosticJsonToClipboard(payload);
+ setMessage({
+ type: "success",
+ text: "纯 JSON 诊断信息已复制",
+ });
+ setTimeout(() => setMessage(null), 2500);
+ } catch (err) {
+ console.error("复制纯 JSON 失败:", err);
+ const isPermissionDenied = isClipboardPermissionDeniedError(err);
+ setShowClipboardGuide(isPermissionDenied);
+ setMessage({
+ type: "error",
+ text: err instanceof Error ? err.message : "复制纯 JSON 失败",
+ });
+ } finally {
+ setDiagnosticBusy(false);
+ }
+ }, [buildDiagnosticPayload]);
+
const exportCrashDiagnostic = useCallback(async () => {
setDiagnosticBusy(true);
setMessage(null);
+ setShowClipboardGuide(false);
try {
const payload = await buildDiagnosticPayload();
- exportCrashDiagnosticToJson(payload);
+ const result = exportCrashDiagnosticToJson(payload, {
+ sceneTag: "settings-experimental",
+ });
+ let openedPath: string | null = null;
+ try {
+ const opened = await openCrashDiagnosticDownloadDirectory();
+ openedPath = opened.openedPath;
+ } catch {
+ openedPath = null;
+ }
setMessage({
type: "success",
- text: "诊断文件已导出,可发送给开发者",
+ text: openedPath
+ ? `诊断文件已导出:${result.fileName},并已打开目录:${openedPath}`
+ : `诊断文件已导出:${result.fileName}(位置:${result.locationHint})`,
});
setTimeout(() => setMessage(null), 2500);
} catch (err) {
@@ -307,6 +357,28 @@ export function ExperimentalSettings() {
}
}, [buildDiagnosticPayload]);
+ const openCrashDownloadDirectory = useCallback(async () => {
+ setDiagnosticBusy(true);
+ setMessage(null);
+ setShowClipboardGuide(false);
+ try {
+ const result = await openCrashDiagnosticDownloadDirectory();
+ setMessage({
+ type: "success",
+ text: `已打开下载目录:${result.openedPath}`,
+ });
+ setTimeout(() => setMessage(null), 2500);
+ } catch (err) {
+ console.error("打开下载目录失败:", err);
+ setMessage({
+ type: "error",
+ text: err instanceof Error ? err.message : "打开下载目录失败",
+ });
+ } finally {
+ setDiagnosticBusy(false);
+ }
+ }, []);
+
// 加载中状态
if (loading) {
return (
@@ -362,6 +434,8 @@ export function ExperimentalSettings() {
)}
+ {showClipboardGuide &&
@@ -562,6 +636,28 @@ export function ExperimentalSettings() {
>
导出诊断 JSON
+
+
+ {/* 响应缓存配置 */}
+