release: v0.1.3
This commit is contained in:
@@ -13,6 +13,7 @@ import AddAccountDialog, {
|
||||
import CourseWorkspace from "~/components/account/CourseWorkspace";
|
||||
import {
|
||||
createWkClient,
|
||||
createSessionWkClient,
|
||||
hostApi,
|
||||
loginApi,
|
||||
runStudyQueue,
|
||||
@@ -93,7 +94,7 @@ const Account = () => {
|
||||
const [showDialog, setShowDialog] = createSignal(false);
|
||||
const [isSubmitting, setIsSubmitting] = createSignal(false);
|
||||
const [loggingOutId, setLoggingOutId] = createSignal("");
|
||||
const [isRefreshingAccount, setIsRefreshingAccount] = createSignal(false);
|
||||
const [refreshingAccountId, setRefreshingAccountId] = createSignal("");
|
||||
const [errorMessage, setErrorMessage] = createSignal("");
|
||||
const [form, setForm] = createSignal<LoginForm>(
|
||||
createDefaultForm("cqcst.leykeji.com"),
|
||||
@@ -300,6 +301,12 @@ const Account = () => {
|
||||
return client;
|
||||
};
|
||||
|
||||
const fetchUserInfoBySession = async (sessionId: string) => {
|
||||
const client = createSessionWkClient(sessionId);
|
||||
const res = await client.userInfoApi();
|
||||
return res.data.user;
|
||||
};
|
||||
|
||||
const reloginBySession = async (sessionId: string) => {
|
||||
if (!sessionId) {
|
||||
return false;
|
||||
@@ -321,14 +328,14 @@ const Account = () => {
|
||||
status: target.status,
|
||||
host: target.host,
|
||||
});
|
||||
const user = await fetchUserInfoBySession(res.data.session_id);
|
||||
|
||||
accountStore.getState().upsertAccount({
|
||||
replaceAccountPreservingView({
|
||||
...target,
|
||||
sessionId: res.data.session_id,
|
||||
user: res.data.user,
|
||||
courses: res.data.courses ?? target.courses,
|
||||
user,
|
||||
courses: target.courses,
|
||||
});
|
||||
await loadCourses(target.id, target.status);
|
||||
appendStudyLog(`会话失效,已重新登录:${target.user.name}`, target.id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -376,11 +383,11 @@ const Account = () => {
|
||||
status: payload.status,
|
||||
host: payload.host,
|
||||
});
|
||||
|
||||
const accountId = `${res.data.user.id}-${payload.host}-${payload.status}`;
|
||||
const user = await fetchUserInfoBySession(res.data.session_id);
|
||||
const accountId = `${user.id}-${payload.host}-${payload.status}`;
|
||||
const nextAccount: AccountItem = {
|
||||
id: accountId,
|
||||
username: payload.username.trim() || res.data.user.id,
|
||||
username: payload.username.trim() || user.id,
|
||||
host: payload.host,
|
||||
status: payload.status,
|
||||
sessionId: res.data.session_id,
|
||||
@@ -388,12 +395,11 @@ const Account = () => {
|
||||
password: payload.password.trim(),
|
||||
token: payload.token.trim(),
|
||||
},
|
||||
user: res.data.user,
|
||||
courses: res.data.courses ?? [],
|
||||
user,
|
||||
courses: [],
|
||||
};
|
||||
|
||||
accountStore.getState().upsertAccount(nextAccount);
|
||||
await loadCourses(nextAccount.id, nextAccount.status);
|
||||
accountStore.getState().setSelectedCourseId(null);
|
||||
accountStore.getState().setRecords([]);
|
||||
setShowDialog(false);
|
||||
@@ -420,6 +426,24 @@ const Account = () => {
|
||||
accountStore.getState().setExpandedAccountId(nextId);
|
||||
};
|
||||
|
||||
const replaceAccountPreservingView = (nextAccount: AccountItem) => {
|
||||
const snapshot = accountStore.getState();
|
||||
const previousSelectedAccountId = snapshot.selectedAccountId;
|
||||
const previousExpandedAccountId = snapshot.expandedAccountId;
|
||||
const previousSelectedCourseId = snapshot.selectedCourseId;
|
||||
|
||||
snapshot.upsertAccount(nextAccount);
|
||||
|
||||
if (previousSelectedAccountId !== nextAccount.id) {
|
||||
snapshot.setSelectedAccountId(previousSelectedAccountId);
|
||||
snapshot.setSelectedCourseId(previousSelectedCourseId);
|
||||
}
|
||||
|
||||
if (previousExpandedAccountId !== nextAccount.id) {
|
||||
snapshot.setExpandedAccountId(previousExpandedAccountId);
|
||||
}
|
||||
};
|
||||
|
||||
const loadCourses = async (
|
||||
accountId = selectedAccount()?.id,
|
||||
status = courseKind(),
|
||||
@@ -465,14 +489,14 @@ const Account = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefreshAccount = async () => {
|
||||
const account = selectedAccount();
|
||||
const handleRefreshAccount = async (accountId: string) => {
|
||||
const account = accounts().find((item) => item.id === accountId);
|
||||
if (!account) {
|
||||
setErrorMessage("请先选择账号。");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRefreshingAccount(true);
|
||||
setRefreshingAccountId(accountId);
|
||||
setErrorMessage("");
|
||||
|
||||
try {
|
||||
@@ -483,20 +507,21 @@ const Account = () => {
|
||||
status: account.status,
|
||||
host: account.host,
|
||||
});
|
||||
const user = await fetchUserInfoBySession(res.data.session_id);
|
||||
|
||||
accountStore.getState().upsertAccount({
|
||||
replaceAccountPreservingView({
|
||||
...account,
|
||||
sessionId: res.data.session_id,
|
||||
user: res.data.user,
|
||||
courses: res.data.courses ?? account.courses,
|
||||
user,
|
||||
courses: account.courses,
|
||||
});
|
||||
await loadCourses(account.id, account.status);
|
||||
await loadCourses(accountId, account.status);
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "刷新账号失败,请稍后重试。";
|
||||
setErrorMessage(message);
|
||||
} finally {
|
||||
setIsRefreshingAccount(false);
|
||||
setRefreshingAccountId("");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -771,11 +796,11 @@ const Account = () => {
|
||||
statusOptions={statusOptions}
|
||||
currentCourseKind={courseKind()}
|
||||
hostLabels={hostLabels()}
|
||||
isRefreshingAccount={isRefreshingAccount()}
|
||||
refreshingAccountId={refreshingAccountId()}
|
||||
loggingOutId={loggingOutId()}
|
||||
densityMode={settingsState().densityMode}
|
||||
sidebarWidth={settingsState().sidebarWidth}
|
||||
onRefreshAccount={() => void handleRefreshAccount()}
|
||||
onRefreshAccount={(accountId) => void handleRefreshAccount(accountId)}
|
||||
onSelectAccount={handleSelectAccount}
|
||||
onToggleExpand={handleToggleExpand}
|
||||
onLogout={(accountId) => void handleLogout(accountId)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
createSignal,
|
||||
For,
|
||||
onCleanup,
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { versionApi } from "~/service/wk";
|
||||
import {
|
||||
fetchDebugLogSnapshot,
|
||||
resolveDebugLogDownloadUrl,
|
||||
@@ -19,6 +17,7 @@ import {
|
||||
import { settingsStore } from "~/store/settings";
|
||||
|
||||
type DebugSocketState = "connecting" | "open" | "closed" | "error";
|
||||
type DebugDetailTab = "overview" | "request" | "response" | "raw";
|
||||
|
||||
const MAX_DEBUG_ENTRIES = 1000;
|
||||
|
||||
@@ -44,18 +43,145 @@ const stringifyDebugFields = (fields?: Record<string, unknown>) => {
|
||||
return JSON.stringify(fields, null, 2);
|
||||
};
|
||||
|
||||
const summarizeDebugFields = (fields?: Record<string, unknown>) => {
|
||||
const value = stringifyDebugFields(fields);
|
||||
if (!value) {
|
||||
return "-";
|
||||
const stringifyDebugValue = (value: unknown) => {
|
||||
if (
|
||||
value === null ||
|
||||
value === undefined ||
|
||||
(typeof value === "string" && value.trim() === "")
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return value.replace(/\s+/g, " ").slice(0, 180);
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
|
||||
return JSON.stringify(value, null, 2);
|
||||
};
|
||||
|
||||
const getField = (value: unknown, path: string[]) => {
|
||||
let current = value;
|
||||
for (const key of path) {
|
||||
if (!current || typeof current !== "object" || !(key in current)) {
|
||||
return undefined;
|
||||
}
|
||||
current = (current as Record<string, unknown>)[key];
|
||||
}
|
||||
return current;
|
||||
};
|
||||
|
||||
const joinPathAndQuery = (path: string, query: string) => {
|
||||
if (!path) {
|
||||
return query ? `?${query}` : "";
|
||||
}
|
||||
return query ? `${path}?${query}` : path;
|
||||
};
|
||||
|
||||
const resolveEntrySummary = (entry: DebugLogEntry) => {
|
||||
const fields = entry.fields;
|
||||
const requestUri = stringifyDebugValue(getField(fields, ["request", "uri"]));
|
||||
if (requestUri) {
|
||||
return requestUri;
|
||||
}
|
||||
|
||||
const requestHost = stringifyDebugValue(getField(fields, ["request", "host"]));
|
||||
if (requestHost) {
|
||||
return requestHost;
|
||||
}
|
||||
|
||||
const path = stringifyDebugValue(fields?.path);
|
||||
const rawQuery = stringifyDebugValue(fields?.rawQuery);
|
||||
const url = joinPathAndQuery(path, rawQuery);
|
||||
if (url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return entry.message || "-";
|
||||
};
|
||||
|
||||
const resolveEntryMethod = (entry: DebugLogEntry) => {
|
||||
return (
|
||||
stringifyDebugValue(getField(entry.fields, ["request", "method"])) ||
|
||||
stringifyDebugValue(entry.fields?.method) ||
|
||||
"-"
|
||||
);
|
||||
};
|
||||
|
||||
const resolveRequestMeta = (entry: DebugLogEntry) => {
|
||||
const fields = entry.fields;
|
||||
const path = stringifyDebugValue(fields?.path);
|
||||
const rawQuery = stringifyDebugValue(fields?.rawQuery);
|
||||
const url = resolveEntrySummary(entry);
|
||||
|
||||
return {
|
||||
method: resolveEntryMethod(entry),
|
||||
url,
|
||||
host: stringifyDebugValue(getField(fields, ["request", "host"])),
|
||||
path: path ? joinPathAndQuery(path, rawQuery) : "",
|
||||
proto:
|
||||
stringifyDebugValue(getField(fields, ["request", "proto"])) ||
|
||||
stringifyDebugValue(fields?.proto),
|
||||
attempt: stringifyDebugValue(getField(fields, ["request", "attempt"])),
|
||||
clientIP: stringifyDebugValue(fields?.clientIP),
|
||||
handler: stringifyDebugValue(fields?.handler),
|
||||
headers:
|
||||
stringifyDebugValue(getField(fields, ["request", "header"])) ||
|
||||
stringifyDebugValue(fields?.requestHeader),
|
||||
body:
|
||||
stringifyDebugValue(getField(fields, ["request", "body"])) ||
|
||||
stringifyDebugValue(fields?.requestBody),
|
||||
};
|
||||
};
|
||||
|
||||
const resolveResponseMeta = (entry: DebugLogEntry) => {
|
||||
const fields = entry.fields;
|
||||
return {
|
||||
status:
|
||||
stringifyDebugValue(getField(fields, ["response", "status"])) ||
|
||||
stringifyDebugValue(fields?.status),
|
||||
statusCode:
|
||||
stringifyDebugValue(getField(fields, ["response", "statusCode"])) ||
|
||||
stringifyDebugValue(fields?.status),
|
||||
proto: stringifyDebugValue(getField(fields, ["response", "proto"])),
|
||||
durationMs:
|
||||
stringifyDebugValue(getField(fields, ["response", "durationMs"])) ||
|
||||
stringifyDebugValue(fields?.durationMs),
|
||||
size:
|
||||
stringifyDebugValue(getField(fields, ["response", "size"])) ||
|
||||
stringifyDebugValue(fields?.responseSize),
|
||||
receivedAt: stringifyDebugValue(getField(fields, ["response", "receivedAt"])),
|
||||
abortWithErrors: stringifyDebugValue(fields?.abortWithErrors),
|
||||
headers:
|
||||
stringifyDebugValue(getField(fields, ["response", "header"])) ||
|
||||
stringifyDebugValue(fields?.responseHeader),
|
||||
body:
|
||||
stringifyDebugValue(getField(fields, ["response", "body"])) ||
|
||||
stringifyDebugValue(fields?.responseBody),
|
||||
};
|
||||
};
|
||||
|
||||
const detailTabs: { id: DebugDetailTab; label: string }[] = [
|
||||
{ id: "overview", label: "概览" },
|
||||
{ id: "request", label: "请求" },
|
||||
{ id: "response", label: "响应" },
|
||||
{ id: "raw", label: "原始字段" },
|
||||
];
|
||||
|
||||
const DetailCodeBlock = (props: { title: string; value: string }) => {
|
||||
return (
|
||||
<div class="rounded-[18px] border border-white/10 bg-zinc-950/90 p-3">
|
||||
<p class="mb-2 text-xs font-medium tracking-[0.2em] text-slate-400 uppercase">
|
||||
{props.title}
|
||||
</p>
|
||||
<pre class="whitespace-pre-wrap break-all text-xs leading-6 text-emerald-200">
|
||||
{props.value || "-"}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DebugLogs = () => {
|
||||
const navigate = useNavigate();
|
||||
const [version] = createResource(versionApi);
|
||||
const [settingsState, setSettingsState] = createSignal(
|
||||
settingsStore.getState(),
|
||||
);
|
||||
@@ -64,6 +190,8 @@ const DebugLogs = () => {
|
||||
createSignal<DebugSocketState>("connecting");
|
||||
const [debugError, setDebugError] = createSignal("");
|
||||
const [selectedDebugEntryId, setSelectedDebugEntryId] = createSignal(0);
|
||||
const [selectedDetailTab, setSelectedDetailTab] =
|
||||
createSignal<DebugDetailTab>("overview");
|
||||
let debugLogContainerRef: HTMLDivElement | undefined;
|
||||
let debugSocket: WebSocket | null = null;
|
||||
let reconnectTimer: number | undefined;
|
||||
@@ -71,7 +199,7 @@ const DebugLogs = () => {
|
||||
const debugEntryKeySet = new Set<number>();
|
||||
|
||||
const isDebugMode = createMemo(() => {
|
||||
return (version()?.data.Mode ?? "").toLowerCase() === "debug";
|
||||
return settingsState().debugEnabled;
|
||||
});
|
||||
const latestDebugEntry = createMemo(() => {
|
||||
const rows = debugEntries();
|
||||
@@ -81,6 +209,14 @@ const DebugLogs = () => {
|
||||
const currentId = selectedDebugEntryId();
|
||||
return debugEntries().find((item) => item.id === currentId) ?? null;
|
||||
});
|
||||
const selectedRequestMeta = createMemo(() => {
|
||||
const entry = selectedDebugEntry();
|
||||
return entry ? resolveRequestMeta(entry) : null;
|
||||
});
|
||||
const selectedResponseMeta = createMemo(() => {
|
||||
const entry = selectedDebugEntry();
|
||||
return entry ? resolveResponseMeta(entry) : null;
|
||||
});
|
||||
const debugSourceCount = createMemo(() => {
|
||||
return new Set(debugEntries().map((item) => item.source)).size;
|
||||
});
|
||||
@@ -196,7 +332,7 @@ const DebugLogs = () => {
|
||||
|
||||
socket.addEventListener("error", () => {
|
||||
setDebugSocketState("error");
|
||||
setDebugError("调试日志流连接失败,请确认后端处于 debug 模式。");
|
||||
setDebugError("调试日志流连接失败,请确认设置页中的调试开关已开启。");
|
||||
});
|
||||
|
||||
socket.addEventListener("close", () => {
|
||||
@@ -228,10 +364,6 @@ const DebugLogs = () => {
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (version.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDebugMode()) {
|
||||
disconnectDebugSocket();
|
||||
resetDebugEntries();
|
||||
@@ -264,6 +396,11 @@ const DebugLogs = () => {
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
selectedDebugEntryId();
|
||||
setSelectedDetailTab("overview");
|
||||
});
|
||||
|
||||
const clearDebugLogs = () => {
|
||||
resetDebugEntries();
|
||||
};
|
||||
@@ -284,7 +421,7 @@ const DebugLogs = () => {
|
||||
后端日志
|
||||
</h1>
|
||||
<p class="mt-1 text-xs leading-5 text-zinc-600 sm:text-sm">
|
||||
独立查看后端 debug 模式下的请求、响应和应用日志。
|
||||
独立查看手动开启调试后的后端请求、响应和应用日志。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -313,7 +450,7 @@ const DebugLogs = () => {
|
||||
when={isDebugMode()}
|
||||
fallback={
|
||||
<div class="mt-3 flex flex-1 items-center justify-center rounded-[26px] border border-dashed border-zinc-300 bg-white/60 px-6 text-zinc-500">
|
||||
当前不是 debug 模式,后端日志页已隐藏。
|
||||
当前未开启调试,请先到设置页手动开启。
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -384,7 +521,7 @@ const DebugLogs = () => {
|
||||
<span>来源</span>
|
||||
<span>等级</span>
|
||||
<span>消息</span>
|
||||
<span>摘要</span>
|
||||
<span>URL / Path</span>
|
||||
</div>
|
||||
<div
|
||||
ref={debugLogContainerRef}
|
||||
@@ -420,9 +557,9 @@ const DebugLogs = () => {
|
||||
</span>
|
||||
<span
|
||||
class="truncate text-slate-400"
|
||||
title={stringifyDebugFields(entry.fields)}
|
||||
title={resolveEntrySummary(entry)}
|
||||
>
|
||||
{summarizeDebugFields(entry.fields)}
|
||||
{resolveEntrySummary(entry)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -436,7 +573,7 @@ const DebugLogs = () => {
|
||||
<div class="border-b border-white/10 px-4 py-3">
|
||||
<p class="text-sm font-semibold text-white">日志详情</p>
|
||||
<p class="mt-1 text-xs text-slate-400">
|
||||
查看当前选中日志的完整字段与上下文。
|
||||
按请求和响应拆分查看当前日志的完整上下文。
|
||||
</p>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto px-4 py-3 font-mono text-sm text-slate-200">
|
||||
@@ -445,24 +582,125 @@ const DebugLogs = () => {
|
||||
fallback={<p class="text-slate-500">请选择左侧一条日志查看详情。</p>}
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<div class="grid gap-2 rounded-[18px] border border-white/10 bg-white/5 px-4 py-3 text-xs text-slate-300">
|
||||
<p>ID: {selectedDebugEntry()?.id}</p>
|
||||
<p>时间: {selectedDebugEntry()?.time}</p>
|
||||
<p>来源: {selectedDebugEntry()?.source}</p>
|
||||
<p>等级: {selectedDebugEntry()?.level}</p>
|
||||
<p>消息: {selectedDebugEntry()?.message}</p>
|
||||
<Show when={selectedDebugEntry()?.caller}>
|
||||
<p>调用位置: {selectedDebugEntry()?.caller}</p>
|
||||
</Show>
|
||||
<Show when={selectedDebugEntry()?.logger}>
|
||||
<p>Logger: {selectedDebugEntry()?.logger}</p>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="rounded-[18px] border border-white/10 bg-zinc-950/90 p-3">
|
||||
<pre class="whitespace-pre-wrap break-all text-xs leading-6 text-emerald-200">
|
||||
{stringifyDebugFields(selectedDebugEntry()?.fields)}
|
||||
</pre>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<For each={detailTabs}>
|
||||
{(tab) => (
|
||||
<button
|
||||
type="button"
|
||||
class={`rounded-full border px-3 py-1.5 text-xs transition ${
|
||||
selectedDetailTab() === tab.id
|
||||
? "border-cyan-300/35 bg-cyan-400/12 text-cyan-100"
|
||||
: "border-white/10 bg-white/5 text-slate-300 hover:bg-white/10"
|
||||
}`}
|
||||
onClick={() => setSelectedDetailTab(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<Show when={selectedDetailTab() === "overview"}>
|
||||
<div class="grid gap-3">
|
||||
<div class="grid gap-2 rounded-[18px] border border-white/10 bg-white/5 px-4 py-3 text-xs text-slate-300">
|
||||
<p>ID: {selectedDebugEntry()?.id}</p>
|
||||
<p>时间: {selectedDebugEntry()?.time}</p>
|
||||
<p>来源: {selectedDebugEntry()?.source}</p>
|
||||
<p>等级: {selectedDebugEntry()?.level}</p>
|
||||
<p>消息: {selectedDebugEntry()?.message}</p>
|
||||
<p>URL / Path: {resolveEntrySummary(selectedDebugEntry()!)}</p>
|
||||
<p>Method: {selectedRequestMeta()?.method || "-"}</p>
|
||||
<Show when={selectedResponseMeta()?.status}>
|
||||
<p>响应状态: {selectedResponseMeta()?.status}</p>
|
||||
</Show>
|
||||
<Show when={selectedResponseMeta()?.durationMs}>
|
||||
<p>耗时: {selectedResponseMeta()?.durationMs} ms</p>
|
||||
</Show>
|
||||
<Show when={selectedDebugEntry()?.caller}>
|
||||
<p>调用位置: {selectedDebugEntry()?.caller}</p>
|
||||
</Show>
|
||||
<Show when={selectedDebugEntry()?.logger}>
|
||||
<p>Logger: {selectedDebugEntry()?.logger}</p>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={selectedDetailTab() === "request"}>
|
||||
<div class="space-y-3">
|
||||
<div class="grid gap-2 rounded-[18px] border border-white/10 bg-white/5 px-4 py-3 text-xs text-slate-300">
|
||||
<p>Method: {selectedRequestMeta()?.method || "-"}</p>
|
||||
<p>URL: {selectedRequestMeta()?.url || "-"}</p>
|
||||
<Show when={selectedRequestMeta()?.path}>
|
||||
<p>Path: {selectedRequestMeta()?.path}</p>
|
||||
</Show>
|
||||
<Show when={selectedRequestMeta()?.host}>
|
||||
<p>Host: {selectedRequestMeta()?.host}</p>
|
||||
</Show>
|
||||
<Show when={selectedRequestMeta()?.proto}>
|
||||
<p>Protocol: {selectedRequestMeta()?.proto}</p>
|
||||
</Show>
|
||||
<Show when={selectedRequestMeta()?.attempt}>
|
||||
<p>Attempt: {selectedRequestMeta()?.attempt}</p>
|
||||
</Show>
|
||||
<Show when={selectedRequestMeta()?.clientIP}>
|
||||
<p>Client IP: {selectedRequestMeta()?.clientIP}</p>
|
||||
</Show>
|
||||
<Show when={selectedRequestMeta()?.handler}>
|
||||
<p>Handler: {selectedRequestMeta()?.handler}</p>
|
||||
</Show>
|
||||
</div>
|
||||
<DetailCodeBlock
|
||||
title="请求头"
|
||||
value={selectedRequestMeta()?.headers || ""}
|
||||
/>
|
||||
<DetailCodeBlock
|
||||
title="请求体"
|
||||
value={selectedRequestMeta()?.body || ""}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={selectedDetailTab() === "response"}>
|
||||
<div class="space-y-3">
|
||||
<div class="grid gap-2 rounded-[18px] border border-white/10 bg-white/5 px-4 py-3 text-xs text-slate-300">
|
||||
<p>响应状态: {selectedResponseMeta()?.status || "-"}</p>
|
||||
<Show when={selectedResponseMeta()?.statusCode}>
|
||||
<p>Status Code: {selectedResponseMeta()?.statusCode}</p>
|
||||
</Show>
|
||||
<Show when={selectedResponseMeta()?.proto}>
|
||||
<p>Protocol: {selectedResponseMeta()?.proto}</p>
|
||||
</Show>
|
||||
<Show when={selectedResponseMeta()?.durationMs}>
|
||||
<p>耗时: {selectedResponseMeta()?.durationMs} ms</p>
|
||||
</Show>
|
||||
<Show when={selectedResponseMeta()?.size}>
|
||||
<p>响应大小: {selectedResponseMeta()?.size}</p>
|
||||
</Show>
|
||||
<Show when={selectedResponseMeta()?.receivedAt}>
|
||||
<p>接收时间: {selectedResponseMeta()?.receivedAt}</p>
|
||||
</Show>
|
||||
<Show when={selectedResponseMeta()?.abortWithErrors}>
|
||||
<p>错误信息: {selectedResponseMeta()?.abortWithErrors}</p>
|
||||
</Show>
|
||||
</div>
|
||||
<DetailCodeBlock
|
||||
title="响应头"
|
||||
value={selectedResponseMeta()?.headers || ""}
|
||||
/>
|
||||
<DetailCodeBlock
|
||||
title="响应体"
|
||||
value={selectedResponseMeta()?.body || ""}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={selectedDetailTab() === "raw"}>
|
||||
<DetailCodeBlock
|
||||
title="原始字段"
|
||||
value={stringifyDebugFields(selectedDebugEntry()?.fields)}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createMemo, createSignal, For, onCleanup, onMount } from "solid-js";
|
||||
import { fetchDebugConfig, updateDebugConfig } from "~/service/debugLog";
|
||||
import { hostApi } from "~/service/wk";
|
||||
import { getMergedHosts, settingsStore } from "~/store/settings";
|
||||
|
||||
@@ -8,6 +9,15 @@ const Setting = () => {
|
||||
const [hostValue, setHostValue] = createSignal("");
|
||||
const [isLoadingRemoteHosts, setIsLoadingRemoteHosts] = createSignal(false);
|
||||
const [hostError, setHostError] = createSignal("");
|
||||
const [debugSyncing, setDebugSyncing] = createSignal(false);
|
||||
const [debugError, setDebugError] = createSignal("");
|
||||
const [backendDebugState, setBackendDebugState] = createSignal<{
|
||||
enabled: boolean;
|
||||
proxy: string;
|
||||
skipSSLVerify: boolean;
|
||||
buildMode: string;
|
||||
proxyConfigured: boolean;
|
||||
} | null>(null);
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = settingsStore.subscribe((nextState) => {
|
||||
@@ -44,6 +54,49 @@ const Setting = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const refreshDebugConfig = async () => {
|
||||
try {
|
||||
const res = await fetchDebugConfig();
|
||||
setBackendDebugState({
|
||||
enabled: res.data.enabled,
|
||||
proxy: res.data.proxy,
|
||||
skipSSLVerify: res.data.skip_ssl_verify,
|
||||
buildMode: res.data.build_mode,
|
||||
proxyConfigured: res.data.proxy_configured,
|
||||
});
|
||||
setDebugError("");
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "获取调试配置失败。";
|
||||
setDebugError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDebugToggle = async (enabled: boolean) => {
|
||||
const previous = state().debugEnabled;
|
||||
settingsStore.getState().setDebugEnabled(enabled);
|
||||
setDebugSyncing(true);
|
||||
setDebugError("");
|
||||
|
||||
try {
|
||||
const res = await updateDebugConfig(enabled);
|
||||
setBackendDebugState({
|
||||
enabled: res.data.enabled,
|
||||
proxy: res.data.proxy,
|
||||
skipSSLVerify: res.data.skip_ssl_verify,
|
||||
buildMode: res.data.build_mode,
|
||||
proxyConfigured: res.data.proxy_configured,
|
||||
});
|
||||
} catch (error) {
|
||||
settingsStore.getState().setDebugEnabled(previous);
|
||||
const message =
|
||||
error instanceof Error ? error.message : "更新调试开关失败。";
|
||||
setDebugError(message);
|
||||
} finally {
|
||||
setDebugSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addLocalHost = () => {
|
||||
if (!hostLabel().trim() || !hostValue().trim()) {
|
||||
return;
|
||||
@@ -61,6 +114,7 @@ const Setting = () => {
|
||||
if (state().remoteHosts.length === 0) {
|
||||
void loadRemoteHosts();
|
||||
}
|
||||
void refreshDebugConfig();
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -208,6 +262,46 @@ const Setting = () => {
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-4">
|
||||
<div class="rounded-2xl border border-zinc-200 bg-zinc-50/80 p-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p class="font-medium text-zinc-900">手动开启调试</p>
|
||||
<p class="mt-1 text-sm text-zinc-500">
|
||||
开启后显示后端日志页,并应用本地代理 / SSL 调试配置
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={state().debugEnabled}
|
||||
disabled={debugSyncing()}
|
||||
onChange={(event) =>
|
||||
void handleDebugToggle(event.currentTarget.checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-3 rounded-xl border border-zinc-200 bg-white px-4 py-3 text-sm text-zinc-600">
|
||||
<p>后端状态:{backendDebugState()?.enabled ? "已开启" : "已关闭"}</p>
|
||||
<p class="mt-1">
|
||||
编译模式:{backendDebugState()?.buildMode ?? "-"}
|
||||
</p>
|
||||
<p class="mt-1">
|
||||
本地代理:
|
||||
{backendDebugState()?.proxyConfigured
|
||||
? backendDebugState()?.proxy
|
||||
: "未配置"}
|
||||
</p>
|
||||
<p class="mt-1">
|
||||
跳过 SSL 校验:
|
||||
{backendDebugState()?.skipSSLVerify ? "已配置" : "未配置"}
|
||||
</p>
|
||||
</div>
|
||||
{debugError() ? (
|
||||
<div class="mt-3 rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-600">
|
||||
{debugError()}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-zinc-200 bg-zinc-50/80 p-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user