From fc394f4412ab495bd2eec138ace706a5679a4068 Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Tue, 3 Mar 2026 00:54:14 +0800 Subject: [PATCH] feat(electron): adapt renderer API and WebSocket layer for dynamic backend URL --- dashboard/src/lib/api.ts | 13 ++++++++++++- dashboard/src/lib/fetch-with-auth.ts | 21 ++++++++++++++++++--- dashboard/src/lib/knowledge-api.ts | 17 +++++++++++++---- dashboard/src/lib/log-websocket.ts | 19 ++++++------------- dashboard/src/lib/plugin-api/marketplace.ts | 7 +++---- dashboard/src/routes/chat/index.tsx | 5 +++-- 6 files changed, 55 insertions(+), 27 deletions(-) diff --git a/dashboard/src/lib/api.ts b/dashboard/src/lib/api.ts index 4c8d1bb9..94a4d94c 100644 --- a/dashboard/src/lib/api.ts +++ b/dashboard/src/lib/api.ts @@ -1,8 +1,19 @@ import axios from 'axios' +import { getApiBaseUrl } from './api-base' + const apiClient = axios.create({ - baseURL: import.meta.env.DEV ? 'http://localhost:8000' : '', + baseURL: '', // 统一为空,通过拦截器动态设置 timeout: 10000, }) +// Electron 端:动态注入后端 URL;浏览器端 getApiBaseUrl() 返回空字符串,行为不变 +apiClient.interceptors.request.use(async (config) => { + const baseUrl = await getApiBaseUrl() + if (baseUrl && !config.baseURL) { + config.baseURL = baseUrl + } + return config +}) + export default apiClient diff --git a/dashboard/src/lib/fetch-with-auth.ts b/dashboard/src/lib/fetch-with-auth.ts index 7a2ef31e..d3529678 100644 --- a/dashboard/src/lib/fetch-with-auth.ts +++ b/dashboard/src/lib/fetch-with-auth.ts @@ -1,5 +1,20 @@ +import { getApiBaseUrl } from './api-base' +import { isElectron } from './runtime' + // 带自动认证处理的 fetch 封装 +/** + * 将相对路径在 Electron 端转换为绝对路径 + * 浏览器端直接返回原始 input,行为不变 + */ +async function resolveUrl(input: RequestInfo | URL): Promise { + if (isElectron() && typeof input === 'string' && input.startsWith('/')) { + const base = await getApiBaseUrl() + return base ? `${base}${input}` : input + } + return input +} + /** * 增强的 fetch 函数,自动处理 401 错误并跳转到登录页 * 使用 HttpOnly Cookie 进行认证,自动携带 credentials @@ -25,7 +40,7 @@ export async function fetchWithAuth( headers, } - const response = await fetch(input, config) + const response = await fetch(await resolveUrl(input), config) // 检测 401 未授权错误 if (response.status === 401) { @@ -54,7 +69,7 @@ export function getAuthHeaders(): HeadersInit { */ export async function logout(): Promise { try { - await fetch('/api/webui/auth/logout', { + await fetch(await resolveUrl('/api/webui/auth/logout'), { method: 'POST', credentials: 'include', }) @@ -70,7 +85,7 @@ export async function logout(): Promise { */ export async function checkAuthStatus(): Promise { try { - const response = await fetch('/api/webui/auth/check', { + const response = await fetch(await resolveUrl('/api/webui/auth/check'), { method: 'GET', credentials: 'include', }) diff --git a/dashboard/src/lib/knowledge-api.ts b/dashboard/src/lib/knowledge-api.ts index b07fe653..ff679191 100644 --- a/dashboard/src/lib/knowledge-api.ts +++ b/dashboard/src/lib/knowledge-api.ts @@ -2,7 +2,16 @@ * 知识库 API */ -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/webui' +import { getApiBaseUrl } from './api-base' +import { isElectron } from './runtime' + +async function getKnowledgeApiBase(): Promise { + if (isElectron()) { + const base = await getApiBaseUrl() + return base ? `${base}/api/webui` : '/api/webui' + } + return import.meta.env.VITE_API_BASE_URL || '/api/webui' +} export interface KnowledgeNode { id: string @@ -35,7 +44,7 @@ export interface KnowledgeStats { * 获取知识图谱数据 */ export async function getKnowledgeGraph(limit: number = 100, nodeType: 'all' | 'entity' | 'paragraph' = 'all'): Promise { - const url = `${API_BASE_URL}/knowledge/graph?limit=${limit}&node_type=${nodeType}` + const url = `${await getKnowledgeApiBase()}/knowledge/graph?limit=${limit}&node_type=${nodeType}` const response = await fetch(url) @@ -50,7 +59,7 @@ export async function getKnowledgeGraph(limit: number = 100, nodeType: 'all' | ' * 获取知识图谱统计信息 */ export async function getKnowledgeStats(): Promise { - const response = await fetch(`${API_BASE_URL}/knowledge/stats`) + const response = await fetch(`${await getKnowledgeApiBase()}/knowledge/stats`) if (!response.ok) { throw new Error('获取知识图谱统计信息失败') } @@ -61,7 +70,7 @@ export async function getKnowledgeStats(): Promise { * 搜索知识节点 */ export async function searchKnowledgeNode(query: string): Promise { - const response = await fetch(`${API_BASE_URL}/knowledge/search?query=${encodeURIComponent(query)}`) + const response = await fetch(`${await getKnowledgeApiBase()}/knowledge/search?query=${encodeURIComponent(query)}`) if (!response.ok) { throw new Error('搜索知识节点失败') } diff --git a/dashboard/src/lib/log-websocket.ts b/dashboard/src/lib/log-websocket.ts index 4ad69127..abf9e7f7 100644 --- a/dashboard/src/lib/log-websocket.ts +++ b/dashboard/src/lib/log-websocket.ts @@ -7,6 +7,8 @@ import { checkAuthStatus } from './fetch-with-auth' import { getSetting } from './settings-manager' import { createReconnectingWebSocket } from './ws-utils' +import { getWsBaseUrl } from '@/lib/api-base' + export interface LogEntry { id: string timestamp: string @@ -54,18 +56,9 @@ class LogWebSocketManager { /** * 获取 WebSocket URL(不含 token 参数) */ - private getWebSocketUrl(): string { - let baseUrl: string - if (import.meta.env.DEV) { - // 开发模式:连接到 WebUI 后端服务器 - baseUrl = 'ws://127.0.0.1:8001/ws/logs' - } else { - // 生产模式:使用当前页面的 host - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' - const host = window.location.host - baseUrl = `${protocol}//${host}/ws/logs` - } - return baseUrl + private async getWebSocketUrl(): Promise { + const wsBase = await getWsBaseUrl() + return `${wsBase}/ws/logs` } /** @@ -85,7 +78,7 @@ class LogWebSocketManager { return } - const wsUrl = this.getWebSocketUrl() + const wsUrl = await this.getWebSocketUrl() // 使用 ws-utils 创建 WebSocket this.wsControl = createReconnectingWebSocket(wsUrl, { diff --git a/dashboard/src/lib/plugin-api/marketplace.ts b/dashboard/src/lib/plugin-api/marketplace.ts index 026e4457..82b8e9c7 100644 --- a/dashboard/src/lib/plugin-api/marketplace.ts +++ b/dashboard/src/lib/plugin-api/marketplace.ts @@ -1,9 +1,9 @@ import type { ApiResponse } from '@/types/api' import type { PluginInfo } from '@/types/plugin' +import { getWsBaseUrl } from '@/lib/api-base' import { fetchWithAuth } from '@/lib/fetch-with-auth' import { parseResponse } from '@/lib/api-helpers' - import type { GitStatus, MaimaiVersion } from './types' /** @@ -213,9 +213,8 @@ export async function connectPluginProgressWebSocket( onProgress: (progress: import('./types').PluginLoadProgress) => void, onError?: (error: Event) => void ): Promise { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' - const host = window.location.host - const wsUrl = `${protocol}//${host}/api/webui/ws/plugin-progress` + const wsBase = await getWsBaseUrl() + const wsUrl = `${wsBase}/api/webui/ws/plugin-progress` // 使用 ws-utils 创建 WebSocket const { createReconnectingWebSocket } = await import('@/lib/ws-utils') diff --git a/dashboard/src/routes/chat/index.tsx b/dashboard/src/routes/chat/index.tsx index 20f45ae4..5d7044a6 100644 --- a/dashboard/src/routes/chat/index.tsx +++ b/dashboard/src/routes/chat/index.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { ScrollArea } from '@/components/ui/scroll-area' import { useToast } from '@/hooks/use-toast' +import { getWsBaseUrl } from '@/lib/api-base' import { fetchWithAuth } from '@/lib/fetch-with-auth' import { cn } from '@/lib/utils' import { Bot, Edit2, Loader2, RefreshCw, User, Send, Wifi, WifiOff, UserCircle2 } from 'lucide-react' @@ -299,7 +300,7 @@ export function ChatPage() { return } - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const wsBase = await getWsBaseUrl() const params = new URLSearchParams() // 添加 token 到参数 @@ -320,7 +321,7 @@ export function ChatPage() { params.append('user_name', userName) } - const wsUrl = `${protocol}//${window.location.host}/api/chat/ws?${params.toString()}` + const wsUrl = `${wsBase}/api/chat/ws?${params.toString()}` console.log(`[Tab ${tabId}] 正在连接 WebSocket:`, wsUrl) try {