- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化) - 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转 - 设置→外观 主题顺序调整为 深色/浅色/跟随系统 - release 二进制重编并验证(浏览器实测关于页链接可开文档页) - 附带工作区中未提交的扁平化重构现状(另一会话进行中) - homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
199 lines
5.6 KiB
TypeScript
199 lines
5.6 KiB
TypeScript
/**
|
|
* 代理错误分层条:proxy | target | auth(产品/设计 P0 不砍)
|
|
* 线框:中文 LayerTag 芯片「代理 / 目标 / 鉴权」;unknown 中性灰
|
|
*/
|
|
import { openExternal } from "../openExternal";
|
|
import { useWorkspace } from "../workspaceStore";
|
|
import { cx } from "./common";
|
|
|
|
export type ProxyErrorLayer = "proxy" | "target" | "auth" | "unknown";
|
|
|
|
export type ClassifiedError = {
|
|
layer: ProxyErrorLayer;
|
|
title: string;
|
|
detail: string;
|
|
actions: { label: string; onClick: () => void; primary?: boolean }[];
|
|
};
|
|
|
|
const LAYER_LABEL: Record<ProxyErrorLayer, string> = {
|
|
proxy: "代理",
|
|
target: "目标",
|
|
auth: "鉴权",
|
|
unknown: "未知",
|
|
};
|
|
|
|
export function classifyRequestError(message: string): ClassifiedError {
|
|
const m = message.toLowerCase();
|
|
|
|
if (
|
|
m.includes("proxy") ||
|
|
m.includes("econnrefused") ||
|
|
m.includes("8787") ||
|
|
m.includes("failed to fetch") ||
|
|
m.includes("networkerror") ||
|
|
m.includes("gin-proxy") ||
|
|
m.includes("load failed")
|
|
) {
|
|
return {
|
|
layer: "proxy",
|
|
title: "代理层失败",
|
|
detail: message,
|
|
actions: [
|
|
{
|
|
label: "检查本地代理",
|
|
primary: true,
|
|
onClick: () => void openExternal("http://127.0.0.1:8787/health"),
|
|
},
|
|
{
|
|
label: "切换桌面直连",
|
|
primary: true,
|
|
onClick: () => {
|
|
useWorkspace.getState().patchSettings({ transportMode: "tauri-native" });
|
|
},
|
|
},
|
|
{
|
|
label: "查看通道说明",
|
|
onClick: () => useWorkspace.getState().setDialog({ type: "settings" }),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
if (
|
|
m.includes("401") ||
|
|
m.includes("403") ||
|
|
m.includes("unauthorized") ||
|
|
m.includes("forbidden") ||
|
|
m.includes("token") ||
|
|
m.includes("鉴权") ||
|
|
(m.includes("auth") && !m.includes("author"))
|
|
) {
|
|
return {
|
|
layer: "auth",
|
|
title: "鉴权层失败",
|
|
detail: message,
|
|
actions: [
|
|
{
|
|
label: "检查 Token / Auth",
|
|
primary: true,
|
|
onClick: () => {
|
|
useWorkspace.getState().setDialog({ type: "environments" });
|
|
},
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
if (
|
|
m.includes("timeout") ||
|
|
m.includes("enotfound") ||
|
|
m.includes("404") ||
|
|
m.includes("502") ||
|
|
m.includes("503") ||
|
|
m.includes("ssl") ||
|
|
m.includes("certificate") ||
|
|
m.includes("target") ||
|
|
m.includes("dns")
|
|
) {
|
|
return {
|
|
layer: "target",
|
|
title: "目标服务失败",
|
|
detail: message,
|
|
actions: [
|
|
{
|
|
label: "检查 BaseURL / VPN",
|
|
primary: true,
|
|
onClick: () => useWorkspace.getState().setDialog({ type: "environments" }),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
return {
|
|
layer: "unknown",
|
|
title: "请求失败(请按层排查)",
|
|
detail: message,
|
|
actions: [
|
|
{
|
|
label: "查代理 :8787",
|
|
primary: true,
|
|
onClick: () => void openExternal("http://127.0.0.1:8787/health"),
|
|
},
|
|
{
|
|
label: "切换桌面直连",
|
|
primary: true,
|
|
onClick: () => useWorkspace.getState().patchSettings({ transportMode: "tauri-native" }),
|
|
},
|
|
{
|
|
label: "查环境 / BaseURL",
|
|
onClick: () => useWorkspace.getState().setDialog({ type: "environments" }),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
const BANNER_STYLE: Record<ProxyErrorLayer, string> = {
|
|
proxy: "border-amber-500/40 bg-amber-500/10 text-amber-50",
|
|
target: "border-sky-500/40 bg-sky-500/10 text-sky-50",
|
|
auth: "border-rose-500/40 bg-rose-500/10 text-rose-50",
|
|
unknown: "border-zinc-500/40 bg-zinc-500/10 text-zinc-100",
|
|
};
|
|
|
|
const TAG_STYLE: Record<ProxyErrorLayer, string> = {
|
|
proxy: "bg-amber-500/25 text-amber-100 ring-amber-400/40",
|
|
target: "bg-sky-500/25 text-sky-100 ring-sky-400/40",
|
|
auth: "bg-rose-500/25 text-rose-100 ring-rose-400/40",
|
|
unknown: "bg-zinc-500/30 text-zinc-200 ring-zinc-400/40",
|
|
};
|
|
|
|
function LayerTag({ layer }: { layer: ProxyErrorLayer }) {
|
|
return (
|
|
<span
|
|
className={cx(
|
|
"inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-semibold tracking-wide ring-1 ring-inset",
|
|
TAG_STYLE[layer],
|
|
)}
|
|
>
|
|
{LAYER_LABEL[layer]}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function ProxyErrorBanner({ message, onDismiss }: { message: string; onDismiss?: () => void }) {
|
|
const info = classifyRequestError(message);
|
|
return (
|
|
<div className={cx("mb-3 rounded-lg border px-3 py-2.5 text-[13px]", BANNER_STYLE[info.layer])}>
|
|
<div className="mb-1.5 flex items-start justify-between gap-2">
|
|
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
<LayerTag layer={info.layer} />
|
|
<span className="font-semibold">{info.title}</span>
|
|
</div>
|
|
{onDismiss && (
|
|
<button type="button" className="ops-btn ops-btn-ghost px-1 py-0 text-xs opacity-70" onClick={onDismiss}>
|
|
✕
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="whitespace-pre-wrap opacity-90">{info.detail}</div>
|
|
<div className="mt-2.5 flex flex-wrap items-center gap-2">
|
|
{info.actions.map((a) =>
|
|
a.primary ? (
|
|
<button key={a.label} type="button" className="ops-btn ops-btn-secondary text-xs" onClick={a.onClick}>
|
|
{a.label}
|
|
</button>
|
|
) : (
|
|
<button
|
|
key={a.label}
|
|
type="button"
|
|
className="text-[12px] underline-offset-2 opacity-80 hover:underline hover:opacity-100"
|
|
onClick={a.onClick}
|
|
>
|
|
{a.label}
|
|
</button>
|
|
),
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|