From 13f0be162b8b37eb5092d25e2b5547a43956a41a Mon Sep 17 00:00:00 2001 From: zhilv Date: Fri, 3 Apr 2026 14:20:26 +0800 Subject: [PATCH] release: v0.1.3 --- package.json | 2 +- src/App.tsx | 33 ++- src/components/account/AccountSidebar.tsx | 25 +- src/pages/accounts/Account.tsx | 67 +++-- src/pages/debug-logs/DebugLogs.tsx | 312 +++++++++++++++++++--- src/pages/settings/Setting.tsx | 94 +++++++ src/service/debugLog.ts | 24 ++ src/service/http.ts | 5 +- src/service/wk.ts | 33 ++- src/store/settings.ts | 4 + 10 files changed, 520 insertions(+), 79 deletions(-) diff --git a/package.json b/package.json index dff997c..345d4c7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.1.2", + "version": "0.1.3", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.tsx b/src/App.tsx index 88a31cf..c6e4953 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,10 +6,12 @@ import { createMemo, createResource, createSignal, + onMount, onCleanup, } from "solid-js"; import { A, useLocation } from "@solidjs/router"; import Dialog from "~/components/dialog/Dialog"; +import { updateDebugConfig } from "~/service/debugLog"; import { RELEASES_PAGE_URL, type LatestRelease, @@ -23,6 +25,7 @@ import { resolveReleaseLink, } from "~/service/update"; import { versionApi } from "~/service/wk"; +import { settingsStore } from "~/store/settings"; type DownloadState = "idle" | "downloading" | "done" | "error"; type UpdateCheckState = @@ -155,7 +158,9 @@ const App: ParentComponent = (props) => { const [downloadState, setDownloadState] = createSignal("idle"); const [downloadProgress, setDownloadProgress] = createSignal(0); const [downloadError, setDownloadError] = createSignal(""); + const [settingsState, setSettingsState] = createSignal(settingsStore.getState()); let updateAbortController: AbortController | null = null; + let lastDebugSyncValue: boolean | undefined; const isActive = (url: string) => location.pathname === url || @@ -170,9 +175,7 @@ const App: ParentComponent = (props) => { const authorText = createMemo(() => version()?.data.GitAuthor ?? "unknown"); const emailText = createMemo(() => version()?.data.GitEmail ?? "unknown"); const modeText = createMemo(() => version()?.data.Mode ?? "unknown"); - const isDebugMode = createMemo( - () => modeText().toLowerCase() === "debug", - ); + const isDebugMode = createMemo(() => settingsState().debugEnabled); const asideList = createMemo(() => { const items = [ { label: "账号", url: "/account" }, @@ -229,6 +232,16 @@ const App: ParentComponent = (props) => { () => latestRelease()?.html_url || RELEASES_PAGE_URL, ); + onMount(() => { + const unsubscribe = settingsStore.subscribe((state) => { + setSettingsState(state); + }); + + onCleanup(() => { + unsubscribe(); + }); + }); + const handleCopyVersion = async () => { try { await navigator.clipboard.writeText(versionPayloadText()); @@ -322,6 +335,17 @@ const App: ParentComponent = (props) => { void performUpdateCheck(false); }); + createEffect(() => { + const enabled = settingsState().debugEnabled; + if (lastDebugSyncValue === enabled) { + return; + } + lastDebugSyncValue = enabled; + void updateDebugConfig(enabled).catch(() => { + // Keep the local preference and let settings page surface sync errors. + }); + }); + onCleanup(() => { if (updateAbortController) { updateAbortController.abort(); @@ -404,6 +428,9 @@ const App: ParentComponent = (props) => {

模式: {modeText()}

+

+ 调试: {isDebugMode() ? "已开启" : "已关闭"} +

Runtime

diff --git a/src/components/account/AccountSidebar.tsx b/src/components/account/AccountSidebar.tsx index 7cd66b1..591d02b 100644 --- a/src/components/account/AccountSidebar.tsx +++ b/src/components/account/AccountSidebar.tsx @@ -14,11 +14,11 @@ interface AccountSidebarProps { statusOptions: StatusOption[]; currentCourseKind: CourseKind; hostLabels: Record; - isRefreshingAccount: boolean; + refreshingAccountId: string; loggingOutId: string; densityMode: "comfortable" | "compact"; sidebarWidth: number; - onRefreshAccount: () => void; + onRefreshAccount: (accountId: string) => void; onSelectAccount: (accountId: string) => void; onToggleExpand: (accountId: string) => void; onLogout: (accountId: string) => void; @@ -42,15 +42,6 @@ const AccountSidebar = (props: AccountSidebarProps) => { 选择账号后查看课程与记录

- -
{ : `登录类型:${statusLabel}`; const badgeTypeLabel = selected() ? currentCourseLabel : statusLabel; const badgeCountLabel = `${account.courses.length} 门`; + const isRefreshing = () => props.refreshingAccountId === account.id; return (
{

{courseTypeLabel}

+
@@ -313,7 +450,7 @@ const DebugLogs = () => { when={isDebugMode()} fallback={
- 当前不是 debug 模式,后端日志页已隐藏。 + 当前未开启调试,请先到设置页手动开启。
} > @@ -384,7 +521,7 @@ const DebugLogs = () => { 来源 等级 消息 - 摘要 + URL / Path
{ - {summarizeDebugFields(entry.fields)} + {resolveEntrySummary(entry)} )} @@ -436,7 +573,7 @@ const DebugLogs = () => {

日志详情

- 查看当前选中日志的完整字段与上下文。 + 按请求和响应拆分查看当前日志的完整上下文。

@@ -445,24 +582,125 @@ const DebugLogs = () => { fallback={

请选择左侧一条日志查看详情。

} >
-
-

ID: {selectedDebugEntry()?.id}

-

时间: {selectedDebugEntry()?.time}

-

来源: {selectedDebugEntry()?.source}

-

等级: {selectedDebugEntry()?.level}

-

消息: {selectedDebugEntry()?.message}

- -

调用位置: {selectedDebugEntry()?.caller}

-
- -

Logger: {selectedDebugEntry()?.logger}

-
-
-
-
-                        {stringifyDebugFields(selectedDebugEntry()?.fields)}
-                      
+
+ + {(tab) => ( + + )} +
+ + +
+
+

ID: {selectedDebugEntry()?.id}

+

时间: {selectedDebugEntry()?.time}

+

来源: {selectedDebugEntry()?.source}

+

等级: {selectedDebugEntry()?.level}

+

消息: {selectedDebugEntry()?.message}

+

URL / Path: {resolveEntrySummary(selectedDebugEntry()!)}

+

Method: {selectedRequestMeta()?.method || "-"}

+ +

响应状态: {selectedResponseMeta()?.status}

+
+ +

耗时: {selectedResponseMeta()?.durationMs} ms

+
+ +

调用位置: {selectedDebugEntry()?.caller}

+
+ +

Logger: {selectedDebugEntry()?.logger}

+
+
+
+
+ + +
+
+

Method: {selectedRequestMeta()?.method || "-"}

+

URL: {selectedRequestMeta()?.url || "-"}

+ +

Path: {selectedRequestMeta()?.path}

+
+ +

Host: {selectedRequestMeta()?.host}

+
+ +

Protocol: {selectedRequestMeta()?.proto}

+
+ +

Attempt: {selectedRequestMeta()?.attempt}

+
+ +

Client IP: {selectedRequestMeta()?.clientIP}

+
+ +

Handler: {selectedRequestMeta()?.handler}

+
+
+ + +
+
+ + +
+
+

响应状态: {selectedResponseMeta()?.status || "-"}

+ +

Status Code: {selectedResponseMeta()?.statusCode}

+
+ +

Protocol: {selectedResponseMeta()?.proto}

+
+ +

耗时: {selectedResponseMeta()?.durationMs} ms

+
+ +

响应大小: {selectedResponseMeta()?.size}

+
+ +

接收时间: {selectedResponseMeta()?.receivedAt}

+
+ +

错误信息: {selectedResponseMeta()?.abortWithErrors}

+
+
+ + +
+
+ + + +
diff --git a/src/pages/settings/Setting.tsx b/src/pages/settings/Setting.tsx index 8aa451b..7b83934 100644 --- a/src/pages/settings/Setting.tsx +++ b/src/pages/settings/Setting.tsx @@ -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 = () => {
+
+
+
+

手动开启调试

+

+ 开启后显示后端日志页,并应用本地代理 / SSL 调试配置 +

+
+ + void handleDebugToggle(event.currentTarget.checked) + } + /> +
+
+

后端状态:{backendDebugState()?.enabled ? "已开启" : "已关闭"}

+

+ 编译模式:{backendDebugState()?.buildMode ?? "-"} +

+

+ 本地代理: + {backendDebugState()?.proxyConfigured + ? backendDebugState()?.proxy + : "未配置"} +

+

+ 跳过 SSL 校验: + {backendDebugState()?.skipSSLVerify ? "已配置" : "未配置"} +

+
+ {debugError() ? ( +
+ {debugError()} +
+ ) : null} +
+
diff --git a/src/service/debugLog.ts b/src/service/debugLog.ts index 2ab61d2..74d48bd 100644 --- a/src/service/debugLog.ts +++ b/src/service/debugLog.ts @@ -1,3 +1,5 @@ +import http from "~/service/http"; + export type DebugLogEntry = { id: number; time: string; @@ -17,6 +19,18 @@ type DebugLogListResponse = { }; }; +type DebugConfigResponse = { + code: number; + message: string; + data: { + enabled: boolean; + proxy: string; + skip_ssl_verify: boolean; + build_mode: string; + proxy_configured: boolean; + }; +}; + const toWsProtocol = (protocol: string) => { return protocol === "https:" ? "wss:" : "ws:"; }; @@ -61,3 +75,13 @@ export const fetchDebugLogSnapshot = async () => { const payload = (await response.json()) as DebugLogListResponse; return payload.data.list ?? []; }; + +export const fetchDebugConfig = async () => { + return await http.get("/api/debug/config"); +}; + +export const updateDebugConfig = async (enabled: boolean) => { + return await http.post("/api/debug/config", { + enabled, + }); +}; diff --git a/src/service/http.ts b/src/service/http.ts index 63c5978..a9b7350 100644 --- a/src/service/http.ts +++ b/src/service/http.ts @@ -5,6 +5,8 @@ type UnauthorizedHandler = (sessionId: string) => Promise; type SessionResolver = () => string | undefined; +const DEFAULT_HTTP_TIMEOUT_MS = 15000; + export type HttpClient = { get(url: string, config?: AxiosRequestConfig): Promise; post(url: string, data?: unknown, config?: AxiosRequestConfig): Promise; @@ -46,7 +48,7 @@ export const createHttpClient = ( ): HttpClient => { const instance = axios.create({ baseURL: import.meta.env.VITE_BASE_URL, - timeout: 15000, + timeout: DEFAULT_HTTP_TIMEOUT_MS, }); instance.interceptors.request.use((config: InternalAxiosRequestConfig) => { @@ -106,3 +108,4 @@ export const createHttpClient = ( const http = createHttpClient(); export default http; +export { DEFAULT_HTTP_TIMEOUT_MS }; diff --git a/src/service/wk.ts b/src/service/wk.ts index a56f6ea..4f5e16c 100644 --- a/src/service/wk.ts +++ b/src/service/wk.ts @@ -1,5 +1,9 @@ import type { Accessor } from "solid-js"; -import http, { createHttpClient, type HttpClient } from "~/service/http"; +import http, { + DEFAULT_HTTP_TIMEOUT_MS, + createHttpClient, + type HttpClient, +} from "~/service/http"; import type { CourseType } from "~/types/Course"; import type { userInfoType } from "~/types/Userinfo"; @@ -24,9 +28,7 @@ export type LoginReq = { }; export type LoginData = { - courses?: CourseType[]; session_id: string; - user: userInfoType; }; export type LoginRes = ApiResponse; @@ -130,6 +132,12 @@ export type CourseData = { export type CourseRes = ApiResponse; +export type UserInfoData = { + user: userInfoType; +}; + +export type UserInfoRes = ApiResponse; + export type StudyReq = { node_id: string; study_id: string; @@ -158,23 +166,34 @@ export type StudyRunnerPayload = { }; export type WkClient = { + userInfoApi: () => Promise; courseApi: (payload: CourseReq) => Promise; recordApi: (payload: RecordReq) => Promise; studyApi: (payload: StudyReq) => Promise; logoutApi: () => Promise; }; +const RECORD_API_TIMEOUT_MS = 60000; +const COURSE_API_TIMEOUT_MS = Math.max(DEFAULT_HTTP_TIMEOUT_MS, 30000); + export const loginApi = async (payload: LoginReq) => { const res = await http.post("/api/login", payload); return res; }; const createWkClientFromHttp = (client: HttpClient): WkClient => ({ + userInfoApi() { + return client.post("/api/v2/userinfo"); + }, courseApi(payload) { - return client.post("/api/v2/course", payload); + return client.post("/api/v2/course", payload, { + timeout: COURSE_API_TIMEOUT_MS, + }); }, recordApi(payload) { - return client.post("/api/v2/record", payload); + return client.post("/api/v2/record", payload, { + timeout: RECORD_API_TIMEOUT_MS, + }); }, studyApi(payload) { return client.post("/api/v2/study", payload); @@ -190,6 +209,10 @@ export const createWkClient = ( return createWkClientFromHttp(createHttpClient(resolveSessionId)); }; +export const createSessionWkClient = (sessionId: string): WkClient => { + return createWkClient(() => sessionId); +}; + const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms)); export const runStudyQueue = async (_payload: StudyRunnerPayload) => { diff --git a/src/store/settings.ts b/src/store/settings.ts index 5ea7522..ceed1f8 100644 --- a/src/store/settings.ts +++ b/src/store/settings.ts @@ -14,6 +14,7 @@ type SettingsState = { persistAccounts: boolean; persistRecords: boolean; persistLogs: boolean; + debugEnabled: boolean; autoScrollLogs: boolean; showLogTimestamps: boolean; densityMode: DensityMode; @@ -24,6 +25,7 @@ type SettingsState = { setPersistSection: (section: CacheSection, value: boolean) => void; clearPersistedSection: (section: CacheSection) => void; clearAllPersistedData: () => void; + setDebugEnabled: (value: boolean) => void; setAutoScrollLogs: (value: boolean) => void; setShowLogTimestamps: (value: boolean) => void; setDensityMode: (value: DensityMode) => void; @@ -93,6 +95,7 @@ export const settingsStore = createStore()( persistAccounts: true, persistRecords: true, persistLogs: true, + debugEnabled: false, autoScrollLogs: true, showLogTimestamps: false, densityMode: "comfortable", @@ -160,6 +163,7 @@ export const settingsStore = createStore()( clearAllPersistedData: () => { localStorage.removeItem(accountStorageKey); }, + setDebugEnabled: (value) => set({ debugEnabled: value }), setAutoScrollLogs: (value) => set({ autoScrollLogs: value }), setShowLogTimestamps: (value) => set({ showLogTimestamps: value }), setDensityMode: (value) => set({ densityMode: value }),